diff --git a/flowey/flowey_hvlite/src/pipelines/cca_fvp.rs b/flowey/flowey_hvlite/src/pipelines/cca_fvp.rs new file mode 100644 index 0000000000..b18fffe6f7 --- /dev/null +++ b/flowey/flowey_hvlite/src/pipelines/cca_fvp.rs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use flowey::node::prelude::ReadVar; +use flowey::pipeline::prelude::*; +use std::path::PathBuf; + +/// Install Shrinkwrap, Build + run CCA FVP via Shrinkwrap (local) +#[derive(clap::Args)] +pub struct CcaFvpCli { + /// Directory for output artifacts/logs (pipeline working dir) + #[clap(long, default_value = "target/cca-fvp")] + pub dir: PathBuf, + + /// Platform YAML (e.g. cca-3world.yaml). If not specified, defaults to cca-3world.yaml + #[clap(long, default_value = "cca-3world.yaml")] + pub platform: PathBuf, + + /// Overlay YAMLs (repeatable), e.g. --overlay buildroot.yaml --overlay planes.yaml + /// If not specified, defaults to buildroot.yaml and planes.yaml + #[clap(long)] + pub overlay: Vec, + + /// Build-time variables (repeatable), e.g. --btvar 'GUEST_ROOTFS=${artifact:BUILDROOT}' + /// If not specified, defaults to GUEST_ROOTFS=${artifact:BUILDROOT} + #[clap(long)] + pub btvar: Vec, + + /// Rootfs path to pass at runtime, e.g. + /// --rootfs /abs/path/.shrinkwrap/package/cca-3world/rootfs.ext2 + /// Default to ${SHRINKWRAP_PACKAGE:-$HOME/.shrinkwrap/package}/cca-3world/rootfs.ext2 + #[clap(long)] + pub rootfs: Option, + + /// Additional runtime variables (repeatable), besides ROOTFS, e.g. --rtvar FOO=bar + #[clap(long)] + pub rtvar: Vec, + + /// Automatically install missing deps (requires sudo on Ubuntu) + #[clap(long, default_value_t = true)] + pub install_missing_deps: bool, + + /// If repo already exists, attempt `git pull --ff-only` + #[clap(long, default_value_t = true)] + pub update_shrinkwrap_repo: bool, + + /// Verbose pipeline output + #[clap(long)] + pub verbose: bool, +} + +impl IntoPipeline for CcaFvpCli { + fn into_pipeline(self, backend_hint: PipelineBackendHint) -> anyhow::Result { + let Self { + dir, + platform, + overlay, + btvar, + rootfs, + rtvar, + install_missing_deps, + update_shrinkwrap_repo, + verbose, + } = self; + + let openvmm_repo = flowey_lib_common::git_checkout::RepoSource::ExistingClone( + ReadVar::from_static(crate::repo_root()), + ); + + let mut pipeline = Pipeline::new(); + + // Store the original dir value for validation before canonicalization + let original_dir = dir.clone(); + + // Convert dir to absolute path to ensure consistency across jobs + // Relative paths are resolved from the repository root + let dir = std::fs::canonicalize(&dir) + .or_else(|_| { + // If dir doesn't exist yet, make it absolute relative to repo root + let abs = if dir.is_absolute() { + dir.clone() + } else { + crate::repo_root().join(&dir) + }; + Ok::<_, anyhow::Error>(abs) + })?; + + // Put Shrinkwrap repo under the pipeline working dir, so it's self-contained. + let shrinkwrap_dir = dir.join("shrinkwrap"); + let shrinkwrap_config_dir = shrinkwrap_dir.join("config"); + + // Helper to resolve platform/overlay paths: + // - Absolute paths: use as-is + // - Simple filenames (no '/'): resolve to /shrinkwrap/config/ + // - Relative paths with '/': must start with --dir prefix + let resolve_config_path = |p: PathBuf, arg_name: &str| -> anyhow::Result { + if p.is_absolute() { + Ok(p) + } else { + let p_str = p.to_string_lossy(); + + // Check if it's a simple filename (no directory separators) + if !p_str.contains('/') { + // Simple filename: resolve to shrinkwrap/config/ + return Ok(shrinkwrap_config_dir.join(p)); + } + + // It's a relative path with directories - validate it starts with --dir + let original_dir_str = original_dir.to_string_lossy(); + let dir_prefix = original_dir_str.trim_start_matches("./"); + let alt_dir_prefix = format!("./{}", dir_prefix); + + if p_str.starts_with(dir_prefix) || p_str.starts_with(&alt_dir_prefix) { + // Valid: path starts with --dir prefix + // Strip the prefix and reconstruct using the canonical dir + let stripped = p_str.strip_prefix(dir_prefix) + .or_else(|| p_str.strip_prefix(alt_dir_prefix.as_str())) + .unwrap() + .trim_start_matches('/'); + + Ok(dir.join(stripped)) + } else { + // Invalid: relative path doesn't start with --dir + anyhow::bail!( + "Relative path for {} must start with the --dir value ({}). Got: {}. \ + Either use an absolute path, a simple filename, or a relative path starting with '{}/'.", + arg_name, original_dir.display(), p.display(), original_dir_str + ) + } + } + }; + + // Apply defaults for options not provided by the user + let overlay = if overlay.is_empty() { + vec![PathBuf::from("buildroot.yaml"), PathBuf::from("planes.yaml")] + } else { + overlay + }; + + let btvar = if btvar.is_empty() { + vec!["GUEST_ROOTFS=${artifact:BUILDROOT}".to_string()] + } else { + btvar + }; + + let rootfs = rootfs.unwrap_or_else(|| { + // First try SHRINKWRAP_PACKAGE env var, then HOME env var + let base_path = std::env::var("SHRINKWRAP_PACKAGE") + .or_else(|_| std::env::var("HOME").map(|h| format!("{}/.shrinkwrap/package", h))) + .expect("Either SHRINKWRAP_PACKAGE or HOME environment variable must be set"); + PathBuf::from(format!("{}/cca-3world/rootfs.ext2", base_path)) + }); + + // Resolve platform YAML path + let platform = resolve_config_path(platform, "--platform")?; + + // Resolve overlay YAML paths + let overlay: Vec = overlay.into_iter() + .map(|p| resolve_config_path(p, "--overlay")) + .collect::>>()?; + + // Create separate jobs to ensure proper ordering + let install_job = pipeline + .new_job( + FlowPlatform::host(backend_hint), + FlowArch::host(backend_hint), + "cca-fvp: install shrinkwrap", + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params { + hvlite_repo_source: openvmm_repo.clone(), + }) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params { + local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams { + interactive: true, + auto_install: install_missing_deps, + force_nuget_mono: false, + external_nuget_auth: false, + ignore_rust_version: true, + }), + verbose: ReadVar::from_static(verbose), + locked: false, + deny_warnings: false, + }) + .dep_on(|ctx| flowey_lib_hvlite::_jobs::local_install_shrinkwrap::Params { + shrinkwrap_dir: shrinkwrap_dir.clone(), + do_installs: install_missing_deps, + update_repo: update_shrinkwrap_repo, + done: ctx.new_done_handle(), + }) + .finish(); + + let build_job = pipeline + .new_job( + FlowPlatform::host(backend_hint), + FlowArch::host(backend_hint), + "cca-fvp: shrinkwrap build", + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params { + hvlite_repo_source: openvmm_repo.clone(), + }) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params { + local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams { + interactive: true, + auto_install: install_missing_deps, + force_nuget_mono: false, + external_nuget_auth: false, + ignore_rust_version: true, + }), + verbose: ReadVar::from_static(verbose), + locked: false, + deny_warnings: false, + }) + .dep_on(|ctx| flowey_lib_hvlite::_jobs::local_shrinkwrap_build::Params { + out_dir: dir.clone(), + shrinkwrap_dir: shrinkwrap_dir.clone(), + platform_yaml: platform.clone(), + overlays: overlay.clone(), + btvars: btvar.clone(), + done: ctx.new_done_handle(), + }) + .finish(); + + // Shrinkwrap run job + let run_job = pipeline + .new_job( + FlowPlatform::host(backend_hint), + FlowArch::host(backend_hint), + "cca-fvp: shrinkwrap run", + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params { + hvlite_repo_source: openvmm_repo.clone(), + }) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params { + local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams { + interactive: true, + auto_install: install_missing_deps, + force_nuget_mono: false, + external_nuget_auth: false, + ignore_rust_version: true, + }), + verbose: ReadVar::from_static(verbose), + locked: false, + deny_warnings: false, + }) + .dep_on(|ctx| flowey_lib_hvlite::_jobs::local_shrinkwrap_run::Params { + out_dir: dir.clone(), + shrinkwrap_dir: shrinkwrap_dir.clone(), + platform_yaml: platform.clone(), + rootfs_path: rootfs.clone(), + rtvars: rtvar.clone(), + done: ctx.new_done_handle(), + }) + .finish(); + + // Explicitly declare job dependencies + pipeline.non_artifact_dep(&build_job, &install_job); + pipeline.non_artifact_dep(&run_job, &build_job); + Ok(pipeline) + } +} diff --git a/flowey/flowey_hvlite/src/pipelines/mod.rs b/flowey/flowey_hvlite/src/pipelines/mod.rs index 54865221e2..26c2127c2f 100644 --- a/flowey/flowey_hvlite/src/pipelines/mod.rs +++ b/flowey/flowey_hvlite/src/pipelines/mod.rs @@ -4,6 +4,7 @@ use flowey::pipeline::prelude::*; use restore_packages::RestorePackagesCli; use vmm_tests::VmmTestsCli; +use cca_fvp::CcaFvpCli; pub mod build_docs; pub mod build_igvm; @@ -11,6 +12,7 @@ pub mod checkin_gates; pub mod custom_vmfirmwareigvm_dll; pub mod restore_packages; pub mod vmm_tests; +pub mod cca_fvp; #[derive(clap::Subcommand)] #[expect(clippy::large_enum_variant)] @@ -34,6 +36,9 @@ pub enum OpenvmmPipelines { /// Build and run VMM tests VmmTests(VmmTestsCli), + + /// Build and run CCA FVP via Shrinkwrap + CcaFvp(CcaFvpCli), } #[derive(clap::Subcommand)] @@ -61,6 +66,7 @@ impl IntoPipeline for OpenvmmPipelines { }, OpenvmmPipelines::RestorePackages(cmd) => cmd.into_pipeline(pipeline_hint), OpenvmmPipelines::VmmTests(cmd) => cmd.into_pipeline(pipeline_hint), + OpenvmmPipelines::CcaFvp(cmd) => cmd.into_pipeline(pipeline_hint), } } } diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_install_shrinkwrap.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_install_shrinkwrap.rs new file mode 100644 index 0000000000..ffb40c3e84 --- /dev/null +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_install_shrinkwrap.rs @@ -0,0 +1,406 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Install Shrinkwrap and its dependencies on Ubuntu. + +use flowey::node::prelude::*; +use flowey::node::prelude::RustRuntimeServices; +use std::path::Path; + +const ARM_GNU_TOOLCHAIN_URL: &str = "https://developer.arm.com/-/media/Files/downloads/gnu/14.3.rel1/binrel/arm-gnu-toolchain-14.3.rel1-x86_64-aarch64-none-elf.tar.xz"; +const OHCL_LINUX_KERNEL_REPO: &str = "https://github.com/weiding-msft/OHCL-Linux-Kernel.git"; +const OHCL_LINUX_KERNEL_PLANE0_BRANCH: &str = "with-arm-rebased-planes"; +const OPENVMM_TMK_REPO: &str = "https://github.com/Flgodd67/openvmm.git"; +const OPENVMM_TMK_BRANCH: &str = "cca-enablement"; +const SHRINKWRAP_REPO: &str = "https://git.gitlab.arm.com/tooling/shrinkwrap.git"; +const CCA_CONFIG_REPO: &str = "https://github.com/weiding-msft/cca_config"; + +const CCA_CONFIGS: &[&str] = &["CONFIG_VIRT_DRIVERS", "CONFIG_ARM_CCA_GUEST"]; +const NINEP_CONFIGS: &[&str] = &[ + "CONFIG_NET_9P", + "CONFIG_NET_9P_FD", + "CONFIG_NET_9P_VIRTIO", + "CONFIG_NET_9P_FS", +]; +const HYPERV_CONFIGS: &[&str] = &[ + "CONFIG_HYPERV", + "CONFIG_HYPERV_MSHV", + "CONFIG_MSHV", + "CONFIG_MSHV_VTL", + "CONFIG_HYPERV_VTL_MODE", +]; + +flowey_request! { + pub struct Params { + /// Directory where shrinkwrap repo will be cloned (e.g. /shrinkwrap) + pub shrinkwrap_dir: PathBuf, + /// If true, run apt-get and pip installs (requires sudo). + /// If false, only clones repo and writes instructions. + pub do_installs: bool, + /// If true, run `git pull --ff-only` if the repo already exists. + pub update_repo: bool, + pub done: WriteVar, + } +} + +new_simple_flow_node!(struct Node); + +///clone or update a git repository +fn clone_or_update_repo( + rt: &RustRuntimeServices<'_>, + repo_url: &str, + target_dir: &Path, + update_repo: bool, + branch: Option<&str>, + repo_name: &str, +) -> anyhow::Result<()> { + if !target_dir.exists() { + log::info!("Cloning {} to {}", repo_name, target_dir.display()); + let mut cmd = flowey::shell_cmd!(rt, "git clone"); + if let Some(b) = branch { + cmd = cmd.args(["--branch", b]); + } + cmd.arg(repo_url).arg(target_dir).run()?; + log::info!("{} cloned successfully", repo_name); + } else if update_repo { + log::info!("Updating {} repo...", repo_name); + rt.sh.change_dir(target_dir); + flowey::shell_cmd!(rt, "git pull --ff-only").run()?; + log::info!("{} updated successfully", repo_name); + } else { + log::info!("{} already exists at {}", repo_name, target_dir.display()); + } + Ok(()) +} + +fn enable_kernel_configs(rt: &RustRuntimeServices<'_>, group: &str, configs: &[&str]) -> anyhow::Result<()> { + // Enable each config one at a time to avoid shell argument parsing issues + for config in configs { + flowey::shell_cmd!(rt, "./scripts/config --file .config --enable {config}") + .run() + .with_context(|| format!("Failed to enable {} kernel config {}", group, config))?; + } + + Ok(()) +} + +/// Build a Rust binary if it doesn't already exist +fn build_rust_binary( + rt: &RustRuntimeServices<'_>, + binary_path: &Path, + package: &str, + build_args: &[&str], +) -> anyhow::Result<()> { + if binary_path.exists() { + log::info!("{} binary already exists at {}", package, binary_path.display()); + return Ok(()); + } + + log::info!("Building {}...", package); + let mut command = flowey::shell_cmd!(rt, "cargo build -p {package}"); + + // Add additional build arguments + for arg in build_args { + command = command.arg(arg); + } + + command + .env("RUSTC_BOOTSTRAP", "1") + .env_remove("ARCH") + .env_remove("CROSS_COMPILE") + .run() + .map_err(|e| anyhow::anyhow!("Failed to build {}: {}", package, e))?; + + log::info!("{} built successfully at: {}", package, binary_path.display()); + Ok(()) +} + +fn make_target(rt: &RustRuntimeServices<'_>, arch: &str, cross_compile: &str, target: &str, jobs: &str) -> anyhow::Result<()> { + flowey::shell_cmd!( + rt, + "make ARCH={arch} CROSS_COMPILE={cross_compile} {target} -j{jobs}" + ) + .run() + .with_context(|| format!("Failed to run `make {}`", target))?; + Ok(()) +} + +impl SimpleFlowNode for Node { + type Request = Params; + + fn imports(_ctx: &mut ImportCtx<'_>) {} + + fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> { + let Params { + shrinkwrap_dir, + do_installs, + update_repo, + done, + } = request; + + ctx.emit_rust_step("install shrinkwrap", |ctx| { + done.claim(ctx); + move |rt| { + + // 0) Create parent dir + if let Some(parent) = shrinkwrap_dir.parent() { + fs_err::create_dir_all(parent)?; + } + + // 1) System deps (Ubuntu) + if do_installs { + log::info!("Installing system dependencies..."); + flowey::shell_cmd!(rt, "sudo apt-get update").run()?; + flowey::shell_cmd!(rt, "sudo apt-get install -y build-essential flex bison libssl-dev libelf-dev bc git netcat-openbsd python3 python3-pip python3-venv telnet docker.io unzip").run()?; + + // Setup Docker group and add current user + log::info!("Setting up Docker group..."); + let username = std::env::var("USER").unwrap_or_else(|_| "vscode".to_string()); + + // Create docker group (ignore error if it already exists) + let _ = flowey::shell_cmd!(rt, "sudo groupadd docker").run(); + + // Add user to docker group + flowey::shell_cmd!(rt, "sudo usermod -aG docker {username}").run()?; + + log::warn!("Docker group membership updated. You may need to log out and log back in for docker permissions to take effect."); + log::warn!("Alternatively, run: newgrp docker"); + } + + // 2) Download and extract ARM GNU toolchain for Host linux kernel compilation + let toolchain_dir = shrinkwrap_dir.parent() + .ok_or_else(|| anyhow::anyhow!("shrinkwrap_dir has no parent"))?; + let toolchain_archive = toolchain_dir.join("arm-gnu-toolchain-14.3.rel1-x86_64-aarch64-none-elf.tar.xz"); + let toolchain_extracted_dir = toolchain_dir.join("arm-gnu-toolchain-14.3.rel1-x86_64-aarch64-none-elf"); + + // Download toolchain if not present + if !toolchain_archive.exists() { + log::info!("Downloading ARM GNU toolchain to {}", toolchain_archive.display()); + flowey::shell_cmd!(rt, "wget -O").arg(&toolchain_archive).arg(ARM_GNU_TOOLCHAIN_URL).run()?; + log::info!("ARM GNU toolchain downloaded successfully"); + } else { + log::info!("ARM GNU toolchain already exists at {}", toolchain_archive.display()); + } + + // Extract toolchain if not already extracted + if !toolchain_extracted_dir.exists() { + log::info!("Extracting ARM GNU toolchain to {}", toolchain_dir.display()); + rt.sh.change_dir(toolchain_dir); + flowey::shell_cmd!(rt, "tar -xvf").arg(&toolchain_archive).run()?; + log::info!("ARM GNU toolchain extracted successfully"); + } else { + log::info!("ARM GNU toolchain already extracted at {}", toolchain_extracted_dir.display()); + } + + // Document the cross-compilation environment variables needed + let cross_compile_path = toolchain_extracted_dir.join("bin").join("aarch64-none-elf-"); + log::info!("ARM GNU toolchain bin path: {}", cross_compile_path.display()); + + // 3) Clone OHCL Linux Kernel (Host Linux Kernel) + let host_kernel_dir = toolchain_dir.join("OHCL-Linux-Kernel"); + clone_or_update_repo( + &rt, + OHCL_LINUX_KERNEL_REPO, + &host_kernel_dir, + update_repo, + Some(OHCL_LINUX_KERNEL_PLANE0_BRANCH), + "OHCL Linux Kernel", + )?; + + // 4) Compile OHCL Linux Kernel with ARM GNU toolchain + let kernel_image = host_kernel_dir.join("arch").join("arm64").join("boot").join("Image"); + if !kernel_image.exists() { + log::info!("Compiling OHCL Linux Kernel..."); + rt.sh.change_dir(&host_kernel_dir); + + // Set environment variables for cross-compilation + let arch = "arm64"; + let cross_compile = cross_compile_path.to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid cross_compile path"))?; + + // Run make defconfig + log::info!("Running make defconfig..."); + make_target(&rt, arch, cross_compile, "defconfig", "1")?; + + // Enable required kernel configs in groups + log::info!("Enabling required kernel configurations..."); + enable_kernel_configs(&rt, "CCA", CCA_CONFIGS)?; + enable_kernel_configs(&rt, "9P", NINEP_CONFIGS)?; + enable_kernel_configs(&rt, "Hyper-V", HYPERV_CONFIGS)?; + + // Run make olddefconfig + log::info!("Running make olddefconfig..."); + make_target(&rt, arch, cross_compile, "olddefconfig", "1")?; + + // Build kernel Image + log::info!("Building kernel Image (this may take several minutes)..."); + let nproc = std::thread::available_parallelism() + .map(|n| n.get().to_string()) + .unwrap_or_else(|_| "1".to_string()); + make_target(&rt, arch, cross_compile, "Image", &nproc)?; + + // Verify kernel Image was created + if !kernel_image.exists() { + anyhow::bail!("Kernel compilation appeared to succeed but Image file was not created at {}", kernel_image.display()); + } + + log::info!("OHCL Linux Kernel compiled successfully"); + log::info!("Kernel Image at: {}", kernel_image.display()); + } else { + log::info!("OHCL Linux Kernel Image already exists at {}", kernel_image.display()); + log::info!("To rebuild, delete the Image file and run again"); + } + + // 4.5) Clone OpenVMM TMK branch with plane0 support and build TMK components + let tmk_kernel_dir = toolchain_dir.join("OpenVMM-TMK"); + clone_or_update_repo( + &rt, + OPENVMM_TMK_REPO, + &tmk_kernel_dir, + update_repo, + Some(OPENVMM_TMK_BRANCH), + "OpenVMM TMK", + )?; + + // Install Rust targets and build TMK components if do_installs is true + if do_installs { + log::info!("Installing Rust cross-compilation targets..."); + flowey::shell_cmd!(rt, "rustup target add aarch64-unknown-linux-gnu").run()?; + flowey::shell_cmd!(rt, "rustup target add aarch64-unknown-none").run()?; + + // Change to the TMK kernel directory (which should be the openvmm repo root) + rt.sh.change_dir(&tmk_kernel_dir); + + log::info!("Building TMK components..."); + + // Build simple_tmk + let simple_tmk_binary = tmk_kernel_dir + .join("target") + .join("aarch64-minimal_rt-none") + .join("debug") + .join("simple_tmk"); + build_rust_binary( + &rt, + &simple_tmk_binary, + "simple_tmk", + &["--config", "openhcl/minimal_rt/aarch64-config.toml"], + )?; + + // Build tmk_vmm + let tmk_vmm_binary = tmk_kernel_dir + .join("target") + .join("aarch64-unknown-linux-gnu") + .join("debug") + .join("tmk_vmm"); + build_rust_binary( + &rt, + &tmk_vmm_binary, + "tmk_vmm", + &["--target", "aarch64-unknown-linux-gnu"], + )?; + + // Return to parent directory + rt.sh.change_dir(shrinkwrap_dir.parent().unwrap()); + } else { + log::info!("Skipping TMK builds (do_installs=false). Run with --install-missing-deps to build."); + } + + // 5) Clone shrinkwrap repo first (need it for venv location) + clone_or_update_repo( + &rt, + SHRINKWRAP_REPO, + &shrinkwrap_dir, + update_repo, + None, + "Shrinkwrap", + )?; + + // 5.5) Clone cca_config repo and copy planes.yaml + let cca_config_dir = toolchain_dir.join("cca_config"); + clone_or_update_repo( + &rt, + CCA_CONFIG_REPO, + &cca_config_dir, + update_repo, + None, + "cca_config", + )?; + + // Copy planes.yaml to shrinkwrap config directory, cca-3world.yaml configuration does not bring + // in the right versions of all the components, this builds a planes-enabled stack + let planes_yaml_src = cca_config_dir.join("planes.yaml"); + let shrinkwrap_config_dir = shrinkwrap_dir.join("config"); + fs_err::create_dir_all(&shrinkwrap_config_dir)?; + let planes_yaml_dest = shrinkwrap_config_dir.join("planes.yaml"); + + if planes_yaml_src.exists() { + log::info!("Copying planes.yaml from {} to {}", + planes_yaml_src.display(), + planes_yaml_dest.display()); + fs_err::copy(&planes_yaml_src, &planes_yaml_dest)?; + } else { + log::warn!("planes.yaml not found in cca_config repo at {}", planes_yaml_src.display()); + } + + // 6) Create Python virtual environment and install deps + let venv_dir = shrinkwrap_dir.join("venv"); + if do_installs { + if !venv_dir.exists() { + log::info!("Creating Python virtual environment at {}", venv_dir.display()); + flowey::shell_cmd!(rt, "python3 -m venv").arg(&venv_dir).run()?; + } + + log::info!("Installing Python dependencies in virtual environment..."); + let pip_bin = venv_dir.join("bin").join("pip"); + flowey::shell_cmd!(rt, "{pip_bin} install --upgrade pip").run()?; + flowey::shell_cmd!(rt, "{pip_bin} install pyyaml termcolor tuxmake").run()?; + } + + // 7) Validate shrinkwrap entrypoint exists + let shrinkwrap_bin_dir = shrinkwrap_dir.join("shrinkwrap"); + if !shrinkwrap_bin_dir.exists() { + anyhow::bail!( + "expected shrinkwrap directory at {}, but it does not exist", + shrinkwrap_bin_dir.display() + ); + } + + // 8) Print PATH guidance + log::info!("=== Setup Complete ==="); + log::info!(""); + log::info!("Shrinkwrap repo ready at: {}", shrinkwrap_dir.display()); + log::info!("Virtual environment at: {}", venv_dir.display()); + log::info!("ARM GNU toolchain ready at: {}", toolchain_extracted_dir.display()); + log::info!("OHCL Linux Kernel ready at: {}", host_kernel_dir.display()); + log::info!("Kernel Image at: {}", kernel_image.display()); + + // Check if TMK binaries exist and report their status + let simple_tmk_binary = tmk_kernel_dir.join("target").join("aarch64-minimal_rt-none").join("debug").join("simple_tmk"); + let tmk_vmm_binary = tmk_kernel_dir.join("target").join("aarch64-unknown-linux-gnu").join("debug").join("tmk_vmm"); + + if simple_tmk_binary.exists() { + log::info!("simple_tmk binary at: {}", simple_tmk_binary.display()); + } + if tmk_vmm_binary.exists() { + log::info!("tmk_vmm binary at: {}", tmk_vmm_binary.display()); + } + + log::info!(""); + log::info!("To use shrinkwrap in your shell:"); + log::info!(" source {}/bin/activate", venv_dir.display()); + log::info!(" export PATH={}:$PATH", shrinkwrap_bin_dir.display()); + log::info!(""); + log::info!("For kernel compilation, set these environment variables:"); + log::info!(" export ARCH=arm64"); + log::info!(" export CROSS_COMPILE={}", cross_compile_path.display()); + log::info!(""); + log::info!("For TMK builds, Rust targets are installed (aarch64-unknown-linux-gnu, aarch64-unknown-none)"); + log::info!("Or the pipeline will invoke it directly using the venv Python."); + + Ok(()) + } + }); + + Ok(()) + } +} diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_shrinkwrap_build.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_shrinkwrap_build.rs new file mode 100644 index 0000000000..7fe45b31b3 --- /dev/null +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_shrinkwrap_build.rs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Run shrinkwrap build command to build FVP artifacts. + +use flowey::node::prelude::*; +use std::io::{BufRead, BufReader, Write}; +use std::process::Stdio; +use std::sync::{Arc, Mutex}; +use std::thread; + +flowey_request! { + pub struct Params { + pub out_dir: PathBuf, + pub shrinkwrap_dir: PathBuf, // Path to shrinkwrap repo (containing shrinkwrap/shrinkwrap executable) + pub platform_yaml: PathBuf, + pub overlays: Vec, + pub btvars: Vec, // "KEY=VALUE" + pub done: WriteVar, + } +} + +new_simple_flow_node!(struct Node); + +impl SimpleFlowNode for Node { + type Request = Params; + + fn imports(_ctx: &mut ImportCtx<'_>) {} + + fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> { + let Params { + out_dir, + shrinkwrap_dir, + platform_yaml, + overlays, + btvars, + done, + } = request; + + ctx.emit_rust_step("run shrinkwrap build", |ctx| { + done.claim(ctx); + move |_rt| { + fs_err::create_dir_all(&out_dir)?; + let log_dir = out_dir.join("logs"); + fs_err::create_dir_all(&log_dir)?; + let log_path = log_dir.join("shrinkwrap-build.log"); + + // Build command line - use shrinkwrap wrapper script with venv activated + let shrinkwrap_exe = shrinkwrap_dir.join("shrinkwrap").join("shrinkwrap"); + let venv_dir = shrinkwrap_dir.join("venv"); + let venv_bin = venv_dir.join("bin"); + + let mut cmd = std::process::Command::new(&shrinkwrap_exe); + cmd.current_dir(&out_dir); // keep build outputs contained + + // Set environment to use venv Python + cmd.env("VIRTUAL_ENV", &venv_dir); + cmd.env("PATH", format!("{}:{}", + venv_bin.display(), + std::env::var("PATH").unwrap_or_default() + )); + + cmd.arg("build"); + cmd.arg(&platform_yaml); + + for ov in &overlays { + cmd.arg("--overlay").arg(ov); + } + + for bt in &btvars { + cmd.arg("--btvar").arg(bt); + } + + // Stream output to both console and log file + log::info!("Running shrinkwrap build..."); + log::info!("Output will be saved to: {}", log_path.display()); + + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + let mut child = cmd.spawn()?; + + let stdout = child.stdout.take() + .ok_or_else(|| anyhow::anyhow!("failed to capture stdout"))?; + let stderr = child.stderr.take() + .ok_or_else(|| anyhow::anyhow!("failed to capture stderr"))?; + + // Open log file + let log_file = Arc::new(Mutex::new( + std::fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&log_path)? + )); + + // Spawn threads to tee output to both console and log file + let log_file_clone = log_file.clone(); + let stdout_thread = thread::spawn(move || { + let reader = BufReader::new(stdout); + for line in reader.lines() { + if let Ok(line) = line { + println!("{}", line); + if let Ok(mut file) = log_file_clone.lock() { + let _ = writeln!(file, "{}", line); + } + } + } + }); + + let log_file_clone = log_file.clone(); + let stderr_thread = thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines() { + if let Ok(line) = line { + eprintln!("{}", line); + if let Ok(mut file) = log_file_clone.lock() { + let _ = writeln!(file, "STDERR: {}", line); + } + } + } + }); + + // Wait for threads to finish + let _ = stdout_thread.join(); + let _ = stderr_thread.join(); + + // Wait for child process + let status = child.wait()?; + + if !status.success() { + anyhow::bail!( + "shrinkwrap build failed (see {})", + log_path.display() + ); + } + + Ok(()) + } + }); + + Ok(()) + } +} diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_shrinkwrap_run.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_shrinkwrap_run.rs new file mode 100644 index 0000000000..4449aa38a0 --- /dev/null +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_shrinkwrap_run.rs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use flowey::node::prelude::*; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +flowey_request! { + /// Parameters for modifying rootfs.ext2 and running shrinkwrap. + pub struct Params { + /// Output directory where shrinkwrap build artifacts are located + pub out_dir: PathBuf, + /// Directory where shrinkwrap repo is cloned + pub shrinkwrap_dir: PathBuf, + /// Platform YAML file for shrinkwrap run + pub platform_yaml: PathBuf, + /// Path to rootfs.ext2 file + pub rootfs_path: PathBuf, + /// Runtime variables for shrinkwrap run (e.g., "ROOTFS=/path/to/rootfs.ext2") + pub rtvars: Vec, + pub done: WriteVar, + } +} + +new_simple_flow_node!(struct Node); + +impl SimpleFlowNode for Node { + type Request = Params; + + fn imports(_ctx: &mut ImportCtx<'_>) {} + + fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> { + let Params { + out_dir, + shrinkwrap_dir, + platform_yaml, + rootfs_path, + rtvars, + done, + } = request; + + ctx.emit_rust_step("modify rootfs.ext2", |ctx| { + done.claim(ctx); + move |_rt| { + // Compute paths the same way as install job + // Get the parent directory (toolchain_dir) where everything is built + let toolchain_dir = shrinkwrap_dir.parent() + .ok_or_else(|| anyhow::anyhow!("shrinkwrap_dir has no parent"))?; + + let tmk_kernel_dir = toolchain_dir.join("OpenVMM-TMK"); + let host_kernel_dir = toolchain_dir.join("OHCL-Linux-Kernel"); + + let simple_tmk = tmk_kernel_dir.join("target/aarch64-minimal_rt-none/debug/simple_tmk"); + let tmk_vmm = tmk_kernel_dir.join("target/aarch64-unknown-linux-gnu/debug/tmk_vmm"); + let kernel_image_path = host_kernel_dir.join("arch/arm64/boot/Image"); + + // Modify rootfs.ext2 to inject TMK binaries and kernel + log::info!("Starting rootfs.ext2 modification..."); + + // Use the rootfs path provided by the user command + let rootfs_ext2 = rootfs_path; + + if !rootfs_ext2.exists() { + anyhow::bail!("rootfs.ext2 not found at {}", rootfs_ext2.display()); + } + + log::info!("Found rootfs.ext2 at {}", rootfs_ext2.display()); + + // Get the directory containing rootfs.ext2 for docker mounting + let rootfs_dir = rootfs_ext2.parent() + .ok_or_else(|| anyhow::anyhow!("rootfs.ext2 has no parent directory"))?; + let rootfs_filename = rootfs_ext2.file_name() + .ok_or_else(|| anyhow::anyhow!("Invalid rootfs path"))? + .to_string_lossy(); + + // Step 1: Run e2fsck to check filesystem + log::info!("Running e2fsck on rootfs.ext2..."); + let e2fsck_status = Command::new("docker") + .args(&["run", "--rm", "-v"]) + .arg(format!("{}:{}", rootfs_dir.display(), rootfs_dir.display())) + .args(&["-w", &rootfs_dir.to_string_lossy()]) + .args(&["ubuntu:24.04", "bash", "-lc"]) + .arg(format!("apt-get update && apt-get install -y e2fsprogs && e2fsck -fp {}", rootfs_filename)) + .status(); + + match e2fsck_status { + Ok(status) if status.success() => log::info!("e2fsck completed successfully"), + Ok(status) => log::warn!("e2fsck exited with status: {}", status), + Err(e) => anyhow::bail!("Failed to run e2fsck: {}", e), + } + + // Step 2: Resize the filesystem + log::info!("Resizing rootfs.ext2 to 1024M..."); + let resize_status = Command::new("docker") + .args(&["run", "--rm", "-v"]) + .arg(format!("{}:{}", rootfs_dir.display(), rootfs_dir.display())) + .args(&["-w", &rootfs_dir.to_string_lossy()]) + .args(&["ubuntu:24.04", "bash", "-lc"]) + .arg(format!("apt-get update && apt-get install -y e2fsprogs && e2fsck -fp {} && resize2fs {} 1024M", rootfs_filename, rootfs_filename)) + .status(); + + match resize_status { + Ok(status) if status.success() => log::info!("resize2fs completed successfully"), + Ok(status) => log::warn!("resize2fs exited with status: {}", status), + Err(e) => anyhow::bail!("Failed to run resize2fs: {}", e), + } + + // Step 3: Mount rootfs, inject files, and unmount + log::info!("Mounting rootfs.ext2 and injecting TMK binaries..."); + + // Use paths from parameters + log::info!("Using simple_tmk from: {}", simple_tmk.display()); + log::info!("Using tmk_vmm from: {}", tmk_vmm.display()); + log::info!("Using kernel Image from: {}", kernel_image_path.display()); + + // Same directory as rootfs.ext2 + let guest_disk = rootfs_dir.join("guest-disk.img"); + let kvmtool_efi = rootfs_dir.join("KVMTOOL_EFI.fd"); + let lkvm = rootfs_dir.join("lkvm"); + + // Copy kernel to Image_ohcl + let image_ohcl = rootfs_dir.join("Image_ohcl"); + if kernel_image_path.exists() { + fs::copy(&kernel_image_path, &image_ohcl) + .map_err(|e| anyhow::anyhow!("Failed to copy kernel Image: {}", e))?; + log::info!("Copied kernel to Image_ohcl"); + } else { + log::warn!("Kernel image not found at {}", kernel_image_path.display()); + } + + // Build the mount/inject script + let mount_script = format!( + r#" + set -e + mkdir -p mnt + mount {rootfs_filename} mnt + mkdir -p mnt/cca + {simple_tmk_copy} + {tmk_vmm_copy} + {guest_disk_copy} + {kvmtool_efi_copy} + {image_ohcl_copy} + {lkvm_copy} + sync + umount mnt || umount -l mnt || true + sync + sleep 1 + # Try multiple times to remove the directory + for i in 1 2 3 4 5; do + if [ -d mnt ]; then + rmdir mnt 2>/dev/null && break || sleep 0.5 + else + break + fi + done + # If still exists, force remove + [ -d mnt ] && rm -rf mnt || true + "#, + rootfs_filename = rootfs_filename, + simple_tmk_copy = if simple_tmk.exists() { + format!("cp {} mnt/cca/", simple_tmk.display()) + } else { + format!("echo 'Warning: {} not found'", simple_tmk.display()) + }, + tmk_vmm_copy = if tmk_vmm.exists() { + format!("cp {} mnt/cca/", tmk_vmm.display()) + } else { + format!("echo 'Warning: {} not found'", tmk_vmm.display()) + }, + guest_disk_copy = if guest_disk.exists() { + format!("cp {} mnt/cca/", guest_disk.display()) + } else { + "".to_string() + }, + kvmtool_efi_copy = if kvmtool_efi.exists() { + format!("cp {} mnt/cca/", kvmtool_efi.display()) + } else { + "".to_string() + }, + image_ohcl_copy = if image_ohcl.exists() { + format!("cp {} mnt/cca/", image_ohcl.display()) + } else { + "".to_string() + }, + lkvm_copy = if lkvm.exists() { + format!("cp {} mnt/cca/", lkvm.display()) + } else { + "".to_string() + }, + ); + + let mount_status = Command::new("sudo") + .arg("bash") + .arg("-c") + .arg(&mount_script) + .current_dir(rootfs_dir) + .status(); + + match mount_status { + Ok(status) if status.success() => { + log::info!("rootfs.ext2 updated successfully with TMK binaries"); + } + Ok(status) => { + anyhow::bail!("Failed to mount/inject files: exit status {}", status); + } + Err(e) => { + anyhow::bail!("Failed to execute mount script: {}", e); + } + } + + // Step 4: Run shrinkwrap with the modified rootfs + log::info!("Running shrinkwrap with platform YAML: {}", platform_yaml.display()); + + // Get the canonical path to rootfs.ext2 + let rootfs_canonical = fs::canonicalize(&rootfs_ext2) + .map_err(|e| anyhow::anyhow!("Failed to canonicalize rootfs path: {}", e))?; + + // Prepare shrinkwrap command + let shrinkwrap_exe = shrinkwrap_dir.join("shrinkwrap").join("shrinkwrap"); + let venv_dir = shrinkwrap_dir.join("venv"); + + if !shrinkwrap_exe.exists() { + anyhow::bail!("shrinkwrap executable not found at {}", shrinkwrap_exe.display()); + } + + // Determine the platform YAML path to use + // If platform_yaml is absolute, try to make it relative to out_dir + // Otherwise, shrinkwrap will look for artifacts relative to the YAML location + let platform_yaml_to_use = if platform_yaml.is_absolute() { + // Try to use just the filename - shrinkwrap should have copied/processed it + platform_yaml.file_name() + .map(|name| PathBuf::from(name)) + .unwrap_or_else(|| platform_yaml.clone()) + } else { + platform_yaml.clone() + }; + + log::info!("Using platform YAML: {} (relative to {})", + platform_yaml_to_use.display(), + out_dir.display()); + + // Build the rtvar arguments + let mut rtvar_args = Vec::new(); + + // Add the ROOTFS rtvar pointing to the modified rootfs.ext2 + rtvar_args.push("--rtvar".to_string()); + rtvar_args.push(format!("ROOTFS={}", rootfs_canonical.display())); + + // Add any additional rtvars from parameters + for rtvar in rtvars { + rtvar_args.push("--rtvar".to_string()); + rtvar_args.push(rtvar); + } + + log::info!("Running: {} run {} {}", + shrinkwrap_exe.display(), + platform_yaml_to_use.display(), + rtvar_args.join(" ")); + + // Set environment to use venv Python + let venv_bin = venv_dir.join("bin"); + + log::info!("Setting VIRTUAL_ENV={}", venv_dir.display()); + + let shrinkwrap_run_status = Command::new(&shrinkwrap_exe) + .arg("run") + .arg(&platform_yaml_to_use) + .args(&rtvar_args) + .env("VIRTUAL_ENV", &venv_dir) + .env("PATH", format!("{}:{}", + venv_bin.display(), + std::env::var("PATH").unwrap_or_default() + )) + .current_dir(&out_dir) // Run from out_dir where build artifacts are + .status(); + + match shrinkwrap_run_status { + Ok(status) if status.success() => { + log::info!("Shrinkwrap run completed successfully"); + } + Ok(status) => { + anyhow::bail!("Shrinkwrap run failed with exit status: {}", status); + } + Err(e) => { + anyhow::bail!("Failed to execute shrinkwrap run: {}", e); + } + } + + Ok(()) + } + }); + + Ok(()) + } +} diff --git a/flowey/flowey_lib_hvlite/src/_jobs/mod.rs b/flowey/flowey_lib_hvlite/src/_jobs/mod.rs index ec3f10a9ea..d7b406402d 100644 --- a/flowey/flowey_lib_hvlite/src/_jobs/mod.rs +++ b/flowey/flowey_lib_hvlite/src/_jobs/mod.rs @@ -26,3 +26,6 @@ pub mod local_custom_vmfirmwareigvm_dll; pub mod local_restore_packages; pub mod publish_vmgstool_gh_release; pub mod test_local_flowey_build_igvm; +pub mod local_install_shrinkwrap; +pub mod local_shrinkwrap_build; +pub mod local_shrinkwrap_run;