Skip to main content

flowey_lib_hvlite/_jobs/
check_distro_build.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Ensure `openvmm` still builds the way a Linux distribution package builds
5//! it.
6//!
7//! This configuration does not use the repository's `.packages/`
8//! provisioning. Every native dependency comes from a distribution package,
9//! and the uploaded vendor archive is consumed exactly the way a packager would
10//! consume it.
11//!
12//! The build runs against a `git archive` export of HEAD rather than the
13//! checkout, both because that is the tree a packager gets from the tag source
14//! archive and because the checkout may be a developer's working tree.
15
16use crate::assemble_openvmm_vendor_release::{
17    CARGO_CONFIG_FILE, VendorReleaseOutput, read_vendor_identity, resolve_identity,
18};
19use flowey::node::prelude::*;
20use std::io::Write;
21
22fn append_vendor_config(config_path: &Path, vendor_config_path: &Path) -> anyhow::Result<()> {
23    let existing = fs_err::read_to_string(config_path)
24        .with_context(|| format!("failed to read {}", config_path.display()))?;
25    let existing = existing
26        .parse::<toml_edit::DocumentMut>()
27        .with_context(|| format!("failed to parse {}", config_path.display()))?;
28    if existing.get("source").is_some() {
29        anyhow::bail!(
30            "{} already defines [source]; refusing to overwrite existing source configuration",
31            config_path.display()
32        );
33    }
34
35    let vendor_config = fs_err::read(vendor_config_path)
36        .with_context(|| format!("failed to read {}", vendor_config_path.display()))?;
37    let mut config = fs_err::OpenOptions::new()
38        .append(true)
39        .open(config_path)
40        .with_context(|| format!("failed to open {}", config_path.display()))?;
41    config.write_all(b"\n")?;
42    config.write_all(&vendor_config)?;
43    Ok(())
44}
45
46flowey_request! {
47    pub struct Request {
48        pub release: ReadVar<VendorReleaseOutput>,
49        pub done: WriteVar<SideEffect>,
50    }
51}
52
53new_simple_flow_node!(struct Node);
54
55impl SimpleFlowNode for Node {
56    type Request = Request;
57
58    fn imports(ctx: &mut ImportCtx<'_>) {
59        ctx.import::<crate::git_checkout_openvmm_repo::Node>();
60        ctx.import::<flowey_lib_common::install_rust::Node>();
61        ctx.import::<flowey_lib_common::install_dist_pkg::Node>();
62    }
63
64    fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
65        let Request { release, done } = request;
66
67        let target = target_lexicon::triple!("x86_64-unknown-linux-gnu");
68        let openvmm_repo_path = ctx.reqv(crate::git_checkout_openvmm_repo::req::GetRepoDir);
69        let rust_toolchain = ctx.reqv(flowey_lib_common::install_rust::Request::GetRustupToolchain);
70
71        // Do not depend on `install_openvmm_rust_build_essential`: it provisions
72        // `protoc` out of `.packages/`, which is what this job exists to avoid.
73        let mut deps = vec![ctx.reqv(flowey_lib_common::install_rust::Request::EnsureInstalled)];
74
75        if matches!(
76            ctx.platform(),
77            FlowPlatform::Linux(FlowPlatformLinuxDistro::Ubuntu)
78        ) {
79            deps.push(
80                ctx.reqv(|v| flowey_lib_common::install_dist_pkg::Request::Install {
81                    package_names: vec![
82                        "build-essential".into(),
83                        "linux-libc-dev".into(),
84                        "libssl-dev".into(),
85                        "pkg-config".into(),
86                        "protobuf-compiler".into(),
87                        "libprotobuf-dev".into(),
88                    ],
89                    done: v,
90                }),
91            );
92        }
93
94        ctx.req(flowey_lib_common::install_rust::Request::InstallTargetTriple(target.clone()));
95
96        ctx.emit_rust_step("build openvmm in a distribution configuration", |ctx| {
97            done.claim(ctx);
98            deps.claim(ctx);
99            let release = release.claim(ctx);
100            let openvmm_repo_path = openvmm_repo_path.claim(ctx);
101            let rust_toolchain = rust_toolchain.claim(ctx);
102            move |rt| {
103                let release = rt.read(release);
104                let openvmm_repo_path = rt.read(openvmm_repo_path);
105                let rust_toolchain = rt.read(rust_toolchain);
106                let identity = read_vendor_identity(&release.assets)?;
107
108                rt.sh.change_dir(&openvmm_repo_path);
109                let checkout_identity = resolve_identity(rt)?;
110                if checkout_identity != identity {
111                    anyhow::bail!(
112                        "vendor archive identity {:?} does not match checkout {:?}",
113                        identity,
114                        checkout_identity
115                    );
116                }
117
118                let archive = release.assets.join(identity.archive_name());
119
120                let build_root = std::env::current_dir()?;
121
122                // Build an export of the commit rather than the checkout
123                // itself. This is the tree a packager actually gets from the
124                // tag source archive, and on the local backend the checkout is
125                // the developer's working tree, which this job would otherwise
126                // fill with a vendored crate tree and a modified tracked
127                // `.cargo/config.toml`.
128                let source_dir = build_root.join("distro-build-source");
129                if source_dir.exists() {
130                    fs_err::remove_dir_all(&source_dir)?;
131                }
132                fs_err::create_dir_all(&source_dir)?;
133
134                let source_tar = build_root.join("distro-build-source.tar");
135                rt.sh.change_dir(&openvmm_repo_path);
136                flowey::shell_cmd!(rt, "git archive --format=tar -o {source_tar} HEAD").run()?;
137                flowey::shell_cmd!(rt, "tar -xf {source_tar} -C {source_dir}").run()?;
138                fs_err::remove_file(&source_tar)?;
139
140                flowey::shell_cmd!(rt, "tar -xzf {archive} -C {source_dir}").run()?;
141
142                let vendor_dir = source_dir.join("vendor");
143                let cargo_config = source_dir.join(CARGO_CONFIG_FILE);
144
145                if !vendor_dir.is_dir() {
146                    anyhow::bail!("vendor archive did not extract {}", vendor_dir.display());
147                }
148
149                if !cargo_config.is_file() {
150                    anyhow::bail!("vendor archive did not extract {}", cargo_config.display());
151                }
152
153                let cargo_config_toml = source_dir.join(".cargo").join("config.toml");
154                append_vendor_config(&cargo_config_toml, &cargo_config)?;
155
156                let cargo_home = build_root.join("distro-cargo-home");
157                if cargo_home.exists() {
158                    fs_err::remove_dir_all(&cargo_home)?;
159                }
160                fs_err::create_dir_all(&cargo_home)?;
161
162                let cargo_target_dir = build_root.join("distro-cargo-target");
163                if cargo_target_dir.exists() {
164                    fs_err::remove_dir_all(&cargo_target_dir)?;
165                }
166                fs_err::create_dir_all(&cargo_target_dir)?;
167
168                // `.cargo/config.toml` does not force its `PROTOC` value, so an
169                // inherited value redirects the build to the system compiler.
170                let protoc = which::which("protoc")
171                    .context("could not find the distribution-provided protoc")?;
172
173                let target = target.to_string();
174                let cargo = if let Some(rust_toolchain) = &rust_toolchain {
175                    flowey::shell_cmd!(rt, "rustup run {rust_toolchain} cargo")
176                } else {
177                    flowey::shell_cmd!(rt, "cargo")
178                };
179
180                rt.sh.change_dir(&source_dir);
181                cargo
182                    .args([
183                        "build",
184                        "--release",
185                        "--locked",
186                        "--offline",
187                        "-p",
188                        "openvmm",
189                        "--target",
190                        &target,
191                    ])
192                    .env("PROTOC", protoc)
193                    .env("OPENSSL_NO_VENDOR", "1")
194                    .env("CARGO_HOME", cargo_home)
195                    .env("CARGO_TARGET_DIR", cargo_target_dir)
196                    // Debug info is not needed for this validation artifact and
197                    // is the binding constraint on runner disk.
198                    .env("CARGO_PROFILE_RELEASE_DEBUG", "0")
199                    .env("CARGO_INCREMENTAL", "0")
200                    .run()?;
201
202                Ok(())
203            }
204        });
205
206        Ok(())
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn appends_vendor_config_without_overwriting_existing_settings() {
216        let dir = tempfile::tempdir().unwrap();
217        let config = dir.path().join("config.toml");
218        let vendor_config = dir.path().join("cargo_config");
219        let existing = "[build]\ntarget-dir = \"target\"\n";
220        let replacement = "[source.crates-io]\nreplace-with = \"vendored-sources\"\n";
221
222        fs_err::write(&config, existing).unwrap();
223        fs_err::write(&vendor_config, replacement).unwrap();
224        append_vendor_config(&config, &vendor_config).unwrap();
225
226        assert_eq!(
227            fs_err::read_to_string(config).unwrap(),
228            format!("{existing}\n{replacement}")
229        );
230    }
231
232    #[test]
233    fn rejects_existing_source_configuration() {
234        let dir = tempfile::tempdir().unwrap();
235        let config = dir.path().join("config.toml");
236        let vendor_config = dir.path().join("cargo_config");
237
238        fs_err::write(&config, "[source.crates-io]\nreplace-with = \"other\"\n").unwrap();
239        fs_err::write(
240            &vendor_config,
241            "[source.vendored-sources]\ndirectory = \"vendor\"\n",
242        )
243        .unwrap();
244
245        assert!(append_vendor_config(&config, &vendor_config).is_err());
246    }
247}