Skip to main content

flowey_lib_hvlite/_jobs/
local_install_cca_emu.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Install CCA emulation environment. Now we only support using ARM's Fixed
5//! Virtual Platform (FVP) as the emulator. The environment also contains a
6//! few essential firmwares in CCA stack, for example TF-A and TF-RMM. ARM has
7//! published an python based tool, 'shrinkwrap', to simply the deployment
8//! process, this installation use it as well.
9use flowey::node::prelude::RustRuntimeServices;
10use flowey::node::prelude::*;
11use std::env;
12use std::path::Path;
13use std::path::PathBuf;
14use std::thread;
15
16const SHRINKWRAP_REPO: &str = "https://git.gitlab.arm.com/tooling/shrinkwrap.git";
17// The guest Linux kernel (with cca/plane driver) hasn't been upstreamed yet, fetch it from our private repo
18const PLANE0_LINUX_REPO: &str = "https://github.com/jiong-microsoft/OHCL-Linux-Kernel.git";
19const PLANE0_LINUX_BRANCH: &str = "cca-dev";
20// A few config information needed when building Linux kernel
21const CCA_CONFIGS: &[&str] = &["CONFIG_VIRT_DRIVERS", "CONFIG_ARM_CCA_GUEST"];
22const NINEP_CONFIGS: &[&str] = &[
23    "CONFIG_NET_9P",
24    "CONFIG_NET_9P_FD",
25    "CONFIG_NET_9P_VIRTIO",
26    "CONFIG_NET_9P_FS",
27];
28const HYPERV_CONFIGS: &[&str] = &[
29    "CONFIG_HYPERV",
30    "CONFIG_HYPERV_MSHV",
31    "CONFIG_MSHV",
32    "CONFIG_MSHV_VTL",
33    "CONFIG_HYPERV_VTL_MODE",
34];
35
36flowey_request! {
37    pub struct Params {
38        /// The CCA test root directory, defaults to target/cca-test.
39        pub test_root: PathBuf,
40        pub openvmm_root: PathBuf,
41        pub done: WriteVar<SideEffect>,
42    }
43}
44
45new_simple_flow_node!(struct Node);
46
47fn enable_kernel_configs(
48    rt: &RustRuntimeServices<'_>,
49    group: &str,
50    configs: &[&str],
51) -> anyhow::Result<()> {
52    // Enable each config one at a time to avoid shell argument parsing issues
53    for config in configs {
54        flowey::shell_cmd!(rt, "./scripts/config --file .config --enable {config}")
55            .run()
56            .with_context(|| format!("Failed to enable {} kernel config {}", group, config))?;
57    }
58
59    Ok(())
60}
61
62fn make_target(
63    rt: &RustRuntimeServices<'_>,
64    arch: &str,
65    target: &str,
66    jobs: &str,
67) -> anyhow::Result<()> {
68    flowey::shell_cmd!(
69        rt,
70        "make ARCH={arch} CROSS_COMPILE=aarch64-linux-gnu- {target} -j{jobs}"
71    )
72    .run()
73    .with_context(|| format!("Failed to run `make {}`", target))?;
74    Ok(())
75}
76
77pub(crate) fn build_plane0_linux(
78    rt: &RustRuntimeServices<'_>,
79    plane0_linux: &Path,
80    plane0_image: &Path,
81) -> anyhow::Result<()> {
82    log::info!("Compiling Plane0 Linux kernel...");
83    rt.sh.change_dir(plane0_linux);
84
85    const ARCH: &str = "arm64";
86    const SINGLE_JOB: &str = "1";
87
88    log::info!("Running make defconfig...");
89    make_target(rt, ARCH, "defconfig", SINGLE_JOB)?;
90
91    log::info!("Enabling required kernel configurations...");
92    for (name, configs) in [
93        ("CCA", CCA_CONFIGS),
94        ("9P", NINEP_CONFIGS),
95        ("Hyper-V", HYPERV_CONFIGS),
96    ] {
97        enable_kernel_configs(rt, name, configs)?;
98    }
99
100    log::info!("Running make olddefconfig...");
101    make_target(rt, ARCH, "olddefconfig", SINGLE_JOB)?;
102
103    let jobs =
104        thread::available_parallelism().map_or_else(|_| "1".to_string(), |n| n.get().to_string());
105
106    log::info!("Building plane0 kernel image...");
107    make_target(rt, ARCH, "Image", &jobs)?;
108
109    anyhow::ensure!(
110        plane0_image.exists(),
111        "Plane0 kernel compilation appeared to succeed but image file was not found at {}",
112        plane0_image.display()
113    );
114
115    log::info!("Plane0 Linux kernel compiled successfully");
116    log::info!("Kernel image at: {}", plane0_image.display());
117    Ok(())
118}
119
120/// Syncs OpenVMM-owned CCA shrinkwrap overlay assets into the shrinkwrap checkout.
121///
122/// Call this before invoking shrinkwrap builds that reference the CCA overlay
123/// assets. The helper ensures `config/` exists under `shrinkwrap_dir` and copies
124/// the repo versions of the assets there, replacing existing files only when
125/// their contents differ.
126pub(crate) fn sync_shrinkwrap_overlay_assets(
127    openvmm_root: &Path,
128    shrinkwrap_dir: &Path,
129) -> anyhow::Result<()> {
130    let overlay_assets = [
131        (
132            openvmm_root.join("vmm_tests/cca_tests/test_data/cca_planes.yaml"),
133            shrinkwrap_dir.join("config/cca_planes.yaml"),
134            "planes.yaml",
135        ),
136        (
137            openvmm_root.join("vmm_tests/cca_tests/test_data/cca_realm_overlay.yaml"),
138            shrinkwrap_dir.join("config/cca_realm_overlay.yaml"),
139            "realm overlay config",
140        ),
141        (
142            openvmm_root.join("vmm_tests/cca_tests/test_data/cca_start_tmk.sh"),
143            shrinkwrap_dir.join("config/cca_start_tmk.sh"),
144            "Plane0 TMK launcher",
145        ),
146    ];
147
148    fs_err::create_dir_all(shrinkwrap_dir.join("config"))?;
149
150    for (src, dest, label) in overlay_assets {
151        if dest.is_file() {
152            if fs_err::read(&src)? == fs_err::read(&dest)? {
153                log::info!(
154                    "{label} already exists at {} and matches source",
155                    dest.display()
156                );
157                continue;
158            }
159
160            log::info!(
161                "{label} already exists at {} but differs from source; replacing it",
162                dest.display()
163            );
164        }
165
166        log::info!(
167            "Copying {label} from {} to {}",
168            src.display(),
169            dest.display()
170        );
171
172        fs_err::copy(&src, &dest)?;
173    }
174
175    Ok(())
176}
177
178pub(crate) fn build_cca_rootfs(
179    rt: &RustRuntimeServices<'_>,
180    test_root: &Path,
181    shrinkwrap_dir: &Path,
182    venv_dir: &Path,
183) -> anyhow::Result<()> {
184    let shrinkwrap_exe = shrinkwrap_dir.join("shrinkwrap/shrinkwrap");
185    anyhow::ensure!(
186        shrinkwrap_exe.exists(),
187        "shrinkwrap installation is missing or broken at {}, try --install-emu first",
188        shrinkwrap_exe.display()
189    );
190    anyhow::ensure!(
191        venv_dir.exists(),
192        "shrinkwrap venv is missing at {}, try --install-emu first",
193        venv_dir.display()
194    );
195
196    let log_dir = test_root.join("logs");
197    fs_err::create_dir_all(&log_dir)?;
198    let log_file = log_dir.join("shrinkwrap.build.log");
199
200    let path = format!(
201        "{}:{}",
202        venv_dir.join("bin").display(),
203        env::var("PATH").unwrap_or_default()
204    );
205
206    let rootfs = "${artifact:BUILDROOT}";
207    let tfa_revision = "8dae0862c502e08568a61a1050091fa9357f1240";
208    let cmd = format!(
209        "{} build cca-3world.yaml \
210        --overlay buildroot.yaml \
211        --overlay cca_realm_overlay.yaml \
212        --overlay cca_planes.yaml \
213        --btvar GUEST_ROOTFS={rootfs} \
214        --btvar TFA_REVISION={tfa_revision} \
215        2>&1 | tee {}",
216        shrinkwrap_exe.display(),
217        log_file.display()
218    );
219
220    flowey::shell_cmd!(rt, "bash -c {cmd}")
221        .env("VIRTUAL_ENV", venv_dir)
222        .env("PATH", path)
223        .run()
224        .with_context(|| "failed to do shrinkwrap build")?;
225
226    log::info!("shrinkwrap build finished, emulation env have been setup");
227    Ok(())
228}
229
230impl SimpleFlowNode for Node {
231    type Request = Params;
232
233    fn imports(ctx: &mut ImportCtx<'_>) {
234        ctx.import::<flowey_lib_common::git_checkout::Node>();
235    }
236
237    fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
238        let Params {
239            test_root,
240            openvmm_root,
241            done,
242        } = request;
243        let plane0_linux = test_root.join("plane0-linux");
244        let shrinkwrap_dir = test_root.join("shrinkwrap");
245
246        let plane0_linux = if plane0_linux.exists() {
247            ReadVar::from_static(plane0_linux)
248        } else {
249            ctx.req(flowey_lib_common::git_checkout::Request::RegisterRepo {
250                repo_id: "cca-plane0-linux".into(),
251                repo_src: flowey_lib_common::git_checkout::RepoSource::LocalOnlyNewClone {
252                    url: PLANE0_LINUX_REPO.into(),
253                    path: plane0_linux,
254                    ignore_existing_clone: false,
255                },
256                allow_persist_credentials: false,
257                depth: None,
258                pre_run_deps: Vec::new(),
259            });
260            ctx.reqv(|v| flowey_lib_common::git_checkout::Request::CheckoutRepo {
261                repo_id: ReadVar::from_static("cca-plane0-linux".into()),
262                repo_path: v,
263                persist_credentials: false,
264            })
265        };
266
267        let shrinkwrap_dir = if shrinkwrap_dir.exists() {
268            ReadVar::from_static(shrinkwrap_dir)
269        } else {
270            ctx.req(flowey_lib_common::git_checkout::Request::RegisterRepo {
271                repo_id: "shrinkwrap".into(),
272                repo_src: flowey_lib_common::git_checkout::RepoSource::LocalOnlyNewClone {
273                    url: SHRINKWRAP_REPO.into(),
274                    path: shrinkwrap_dir,
275                    ignore_existing_clone: false,
276                },
277                allow_persist_credentials: false,
278                depth: None,
279                pre_run_deps: Vec::new(),
280            });
281            ctx.reqv(|v| flowey_lib_common::git_checkout::Request::CheckoutRepo {
282                repo_id: ReadVar::from_static("shrinkwrap".into()),
283                repo_path: v,
284                persist_credentials: false,
285            })
286        };
287
288        ctx.emit_rust_step("install cca emulation environment", |ctx| {
289            done.claim(ctx);
290            let plane0_linux = plane0_linux.claim(ctx);
291            let shrinkwrap_dir = shrinkwrap_dir.claim(ctx);
292            move |rt| {
293                // emulation environment is under 'test_root'
294                fs_err::create_dir_all(&test_root)?;
295
296                // 'shrinkwrap' only build host Linux kernel, plane0 Linux kernel
297                // needs to be downloaded and built separately.
298                let plane0_linux = rt.read(plane0_linux);
299                let plane0_image = plane0_linux
300                    .join("arch")
301                    .join("arm64")
302                    .join("boot")
303                    .join("Image");
304                rt.sh.change_dir(&plane0_linux);
305                flowey::shell_cmd!(rt, "git checkout {PLANE0_LINUX_BRANCH}").run()?;
306
307                // Now check if image has been built
308                if plane0_image.exists() {
309                    log::info!(
310                        "plane0 Linux image also has been built and found at: {}",
311                        plane0_image.display()
312                    );
313                } else {
314                    build_plane0_linux(rt, &plane0_linux, &plane0_image)?;
315                }
316
317                // Install the remaining emulation environment components
318                // using 'shrinkwrap', which leverages YAML to define all required
319                // components. This significantly reduces manual effort and the risk of errors.
320                let shrinkwrap_dir = rt.read(shrinkwrap_dir);
321                let venv_dir = shrinkwrap_dir.join("venv");
322                if !venv_dir.exists() {
323                    log::info!(
324                        "Creating Python virtual environment at {}",
325                        venv_dir.display()
326                    );
327                    flowey::shell_cmd!(rt, "python3 -m venv")
328                        .arg(&venv_dir)
329                        .run()?;
330
331                    log::info!("Installing Python dependencies...");
332                    let pip = venv_dir.join("bin/pip");
333
334                    flowey::shell_cmd!(rt, "{pip} install --upgrade pip").run()?;
335                    flowey::shell_cmd!(rt, "{pip} install pyyaml termcolor tuxmake").run()?;
336                }
337
338                sync_shrinkwrap_overlay_assets(&openvmm_root, &shrinkwrap_dir)?;
339
340                let home_dir = env::var("HOME").map(PathBuf::from).expect("HOME not set");
341                let rootfs_file = home_dir.join(".shrinkwrap/package/cca-3world/rootfs.ext2");
342                if rootfs_file.exists() {
343                    log::info!(
344                        "cca emulation rootfs is already generated at: {}",
345                        rootfs_file.display()
346                    );
347                } else {
348                    build_cca_rootfs(rt, &test_root, &shrinkwrap_dir, &venv_dir)?;
349                }
350
351                Ok(())
352            }
353        });
354
355        Ok(())
356    }
357}