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                    ],
88                    done: v,
89                }),
90            );
91        }
92
93        ctx.req(flowey_lib_common::install_rust::Request::InstallTargetTriple(target.clone()));
94
95        ctx.emit_rust_step("build openvmm in a distribution configuration", |ctx| {
96            done.claim(ctx);
97            deps.claim(ctx);
98            let release = release.claim(ctx);
99            let openvmm_repo_path = openvmm_repo_path.claim(ctx);
100            let rust_toolchain = rust_toolchain.claim(ctx);
101            move |rt| {
102                let release = rt.read(release);
103                let openvmm_repo_path = rt.read(openvmm_repo_path);
104                let rust_toolchain = rt.read(rust_toolchain);
105                let identity = read_vendor_identity(&release.assets)?;
106
107                rt.sh.change_dir(&openvmm_repo_path);
108                let checkout_identity = resolve_identity(rt)?;
109                if checkout_identity != identity {
110                    anyhow::bail!(
111                        "vendor archive identity {:?} does not match checkout {:?}",
112                        identity,
113                        checkout_identity
114                    );
115                }
116
117                let archive = release.assets.join(identity.archive_name());
118
119                let build_root = std::env::current_dir()?;
120
121                // Build an export of the commit rather than the checkout
122                // itself. This is the tree a packager actually gets from the
123                // tag source archive, and on the local backend the checkout is
124                // the developer's working tree, which this job would otherwise
125                // fill with a vendored crate tree and a modified tracked
126                // `.cargo/config.toml`.
127                let source_dir = build_root.join("distro-build-source");
128                if source_dir.exists() {
129                    fs_err::remove_dir_all(&source_dir)?;
130                }
131                fs_err::create_dir_all(&source_dir)?;
132
133                let source_tar = build_root.join("distro-build-source.tar");
134                rt.sh.change_dir(&openvmm_repo_path);
135                flowey::shell_cmd!(rt, "git archive --format=tar -o {source_tar} HEAD").run()?;
136                flowey::shell_cmd!(rt, "tar -xf {source_tar} -C {source_dir}").run()?;
137                fs_err::remove_file(&source_tar)?;
138
139                flowey::shell_cmd!(rt, "tar -xzf {archive} -C {source_dir}").run()?;
140
141                let vendor_dir = source_dir.join("vendor");
142                let cargo_config = source_dir.join(CARGO_CONFIG_FILE);
143
144                if !vendor_dir.is_dir() {
145                    anyhow::bail!("vendor archive did not extract {}", vendor_dir.display());
146                }
147
148                if !cargo_config.is_file() {
149                    anyhow::bail!("vendor archive did not extract {}", cargo_config.display());
150                }
151
152                let cargo_config_toml = source_dir.join(".cargo").join("config.toml");
153                append_vendor_config(&cargo_config_toml, &cargo_config)?;
154
155                let cargo_home = build_root.join("distro-cargo-home");
156                if cargo_home.exists() {
157                    fs_err::remove_dir_all(&cargo_home)?;
158                }
159                fs_err::create_dir_all(&cargo_home)?;
160
161                let cargo_target_dir = build_root.join("distro-cargo-target");
162                if cargo_target_dir.exists() {
163                    fs_err::remove_dir_all(&cargo_target_dir)?;
164                }
165                fs_err::create_dir_all(&cargo_target_dir)?;
166
167                // `.cargo/config.toml` does not force its `PROTOC` value, so an
168                // inherited value redirects the build to the system compiler.
169                let protoc = which::which("protoc")
170                    .context("could not find the distribution-provided protoc")?;
171
172                let target = target.to_string();
173                let cargo = if let Some(rust_toolchain) = &rust_toolchain {
174                    flowey::shell_cmd!(rt, "rustup run {rust_toolchain} cargo")
175                } else {
176                    flowey::shell_cmd!(rt, "cargo")
177                };
178
179                rt.sh.change_dir(&source_dir);
180                cargo
181                    .args([
182                        "build",
183                        "--release",
184                        "--locked",
185                        "--offline",
186                        "-p",
187                        "openvmm",
188                        "--target",
189                        &target,
190                    ])
191                    .env("PROTOC", protoc)
192                    .env("OPENSSL_NO_VENDOR", "1")
193                    .env("CARGO_HOME", cargo_home)
194                    .env("CARGO_TARGET_DIR", cargo_target_dir)
195                    // Debug info is not needed for this validation artifact and
196                    // is the binding constraint on runner disk.
197                    .env("CARGO_PROFILE_RELEASE_DEBUG", "0")
198                    .env("CARGO_INCREMENTAL", "0")
199                    .run()?;
200
201                Ok(())
202            }
203        });
204
205        Ok(())
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn appends_vendor_config_without_overwriting_existing_settings() {
215        let dir = tempfile::tempdir().unwrap();
216        let config = dir.path().join("config.toml");
217        let vendor_config = dir.path().join("cargo_config");
218        let existing = "[build]\ntarget-dir = \"target\"\n";
219        let replacement = "[source.crates-io]\nreplace-with = \"vendored-sources\"\n";
220
221        fs_err::write(&config, existing).unwrap();
222        fs_err::write(&vendor_config, replacement).unwrap();
223        append_vendor_config(&config, &vendor_config).unwrap();
224
225        assert_eq!(
226            fs_err::read_to_string(config).unwrap(),
227            format!("{existing}\n{replacement}")
228        );
229    }
230
231    #[test]
232    fn rejects_existing_source_configuration() {
233        let dir = tempfile::tempdir().unwrap();
234        let config = dir.path().join("config.toml");
235        let vendor_config = dir.path().join("cargo_config");
236
237        fs_err::write(&config, "[source.crates-io]\nreplace-with = \"other\"\n").unwrap();
238        fs_err::write(
239            &vendor_config,
240            "[source.vendored-sources]\ndirectory = \"vendor\"\n",
241        )
242        .unwrap();
243
244        assert!(append_vendor_config(&config, &vendor_config).is_err());
245    }
246}