Rust/cargo-azsphere/src/package.rs (297 lines of code) (raw):

use crate::config::{Config, ExtraMetadataSource}; use crate::error::{self, ConfigError}; use anyhow::Context; use serde_json::Value; use std::env; use std::fs; use std::io::{self, Write}; use std::path::PathBuf; use std::process::Command; #[derive(clap::Parser, Debug)] #[group(skip)] pub(crate) struct CliArgs { /// set a package name of the workspace #[arg(short, long)] pub(crate) package: Option<String>, /// build artifacts in release mode, with optimizations [DEFAULT = false] #[arg(short, long)] pub(crate) release: bool, /// overwrite metadata with TOML text. #[arg(short, long)] set_metadata: Vec<String>, /// overwrite metadata with TOML file. If suffixed with "#dotted.key", load "dotted.key" table instead of the root table. #[arg(long)] metadata_overwrite: Vec<String>, /// shortcut to --metadata-overwrite=path/to/Cargo.toml#package.metadata.generate-rpm.variants.VARIANT #[arg(long)] variant: Option<String>, /// enable verbose logging #[arg(short)] pub(crate) verbose: bool, } impl CliArgs { pub(crate) fn extra_metadata(&self) -> Vec<ExtraMetadataSource> { if self.verbose { println!("Collecting extra_metadata"); } let mut extra_metadata = self .metadata_overwrite .iter() .enumerate() .map(|(i, v)| { let (file, branch) = match v.split_once("#") { None => (PathBuf::from(v), None), Some((file, branch)) => (PathBuf::from(file), Some(branch.to_string())), }; (i, ExtraMetadataSource::File(file, branch)) }) .chain( self.set_metadata .iter() .enumerate() .map(|(i, v)| (i, ExtraMetadataSource::Text(v.clone()))), ) .chain(self.variant.iter().enumerate().map(|(i, v)| { let file = match &self.package { None => PathBuf::from("Cargo.toml"), Some(package) => PathBuf::from(package).join("Cargo.toml"), }; let branch = format!("package.metadata.generate-rpm.variants.{v}"); (i, ExtraMetadataSource::File(file, Some(branch))) })) .collect::<Vec<_>>(); extra_metadata.sort_by_key(|(i, _)| *i); extra_metadata.into_iter().map(|(_, v)| v).collect() } } #[derive(Debug)] pub struct CliSetting { release: bool, verbose: bool, extra_metadata: Vec<ExtraMetadataSource>, } impl CliSetting { pub(crate) fn new(args: CliArgs) -> Self { let extra_metadata = args.extra_metadata(); Self { release: args.release, verbose: args.verbose, extra_metadata, } } pub fn build_app_package(self) -> anyhow::Result<()> { if self.verbose { println!("Invoking 'cargo metadata'"); } let output = Command::new("cargo") .arg("metadata") .arg("--format-version=1") .output() .expect("Failed to run 'cargo metadata'"); if self.verbose { println!("Parsing metadata"); } let cargo_metadata: Value = serde_json::from_slice(output.stdout.as_slice()).unwrap(); if self.verbose { println!("Finding package config"); } let manifest_file_dir = env::current_dir().context("failed to get manifest file")?; let manifest_file_path = manifest_file_dir.join("Cargo.toml"); let config = Config::new(&manifest_file_path, self.extra_metadata.as_slice())?; let package_config = config.package_config( cargo_metadata["workspace_root"] .to_string() .replace("\"", ""), self.verbose, )?; // cargo_metadata["target_directory"] + "armv7-unknown-linux-musleabihf" + "debug" + crate_name is the executable file if self.verbose { println!("Determining target_path"); } let build_flavor = if self.release { "release" } else { "debug" }; let target_directory = cargo_metadata["target_directory"] .to_string() .replace("\"", ""); let mut target_path = PathBuf::from(target_directory); target_path.push("armv7-unknown-linux-musleabihf"); target_path.push(build_flavor); let source_program = target_path.join(&package_config.name); let out_dir = target_path.join("out"); let dest_dir = out_dir.join(&package_config.name); if self.verbose { println!("Staging files to {}", dest_dir.display()); } let _ = fs::remove_dir_all(&dest_dir); // Remove the previous contents. Ignore failures, which can happen if the dir doesn't exist. fs::create_dir_all(&dest_dir)?; // Create a clean directory let dest_bin_dir = dest_dir.join("bin"); fs::create_dir_all(&dest_bin_dir)?; // cp app_manifest.json out/ let dest_app_manifest = dest_dir.join("app_manifest.json"); if self.verbose { println!("Copy app manifest {}", &package_config.app_manifest); } fs::copy(&package_config.app_manifest, &dest_app_manifest) .context("failed to copy app manifest")?; // cp ../target/armv7-unknown-linux-musleabihf/debug/${APPNAME} out/bin let dest_program = dest_bin_dir.join(&package_config.name); if self.verbose { println!( "Copying app executable from {} to {}", source_program.display(), dest_program.display() ); } if !source_program.exists() { anyhow::bail!( "executable `{}` does not exist. Are you sure you compiled it with `cargo build`?", source_program.display() ); } fs::copy(&source_program, &dest_program).with_context(|| { format!( "failed to copy executable from {} to {}", source_program.display(), dest_program.display() ) })?; // patchelf --set-interpreter /lib/ld-musl-armhf.so.1 out/bin/${APPNAME} if self.verbose { println!("Running patchelf"); } let output = Command::new("patchelf") .arg("--set-interpreter") .arg("/lib/ld-musl-armhf.so.1") .arg(dest_program.as_os_str()) .output() .expect("Failed to execute patchelf"); if !output.status.success() { println!("patchelf command failed:"); io::stdout().write_all(&output.stdout).unwrap(); io::stderr().write_all(&output.stderr).unwrap(); std::process::exit(output.status.code().unwrap()); } // ${AzureSphereDefaultSDKDir}/Sysroots/${ARV}/tools/sysroots/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-musleabi/arm-poky-linux-musleabi-strip --strip-debug --strip-unneeded out/bin/${APPNAME} let sdk_path = PathBuf::from(std::env::var("AzureSphereDefaultSDKDir").unwrap()); let strip_command = sdk_path.join("Sysroots").join(&package_config.arv).join("tools/sysroots/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-musleabi/arm-poky-linux-musleabi-strip"); if self.verbose { println!("Running arm-poky-linux-musleabi-strip"); } let output = Command::new(strip_command) .arg("--strip-debug") .arg("--strip-unneeded") .arg(dest_program.as_os_str()) .output() .expect("Failed to execute strip"); if !output.status.success() { println!("patchelf command failed:"); io::stdout().write_all(&output.stdout).unwrap(); io::stderr().write_all(&output.stderr).unwrap(); std::process::exit(output.status.code().unwrap()); } // Copy extra files if let Some(extra_files) = package_config.extra_files { for extra_file in extra_files { let extra_file = extra_file.as_array().ok_or(ConfigError::WrongType( "extra_files entry".to_string(), "array", ))?; if extra_file.len() < 1 || extra_file.len() > 2 { return Err(error::Error::Config(ConfigError::MalFormedArray).into()); } let source_file = extra_file[0].as_str().ok_or(ConfigError::WrongType( "extra_files entry source filename".to_string(), "string", ))?; let destination_file = if extra_file.len() == 1 { source_file } else { extra_file[1].as_str().ok_or(ConfigError::WrongType( "extra_files entry destination filename".to_string(), "string", ))? }; let source_file_path = manifest_file_dir.join(source_file); let destination_file_path = dest_dir.join(destination_file); if self.verbose { println!( "Copying extra_file {} => {} ", source_file, destination_file ); } let destination_file_directory = destination_file_path.parent().unwrap(); fs::create_dir_all(destination_file_directory)?; fs::copy(source_file_path, destination_file_path)?; } } let target_definition_file = if let Some(target_definition) = &package_config.target_definition { let t = target_definition.to_owned() + ".json"; Some(t) } else { None }; // ${AzureSphereDefaultSDKDir}/Tools_v2/azsphere image-package pack-application --package-directory out/ --destination ${APPNAME}.imagepackage --target-api-set ${ARV} let azsphere = sdk_path.join("Tools_v2/azsphere"); let app_package_name = target_path.join(package_config.name.clone() + ".imagepackage"); let mut args = vec![ "image-package", "pack-application", "--package-directory", dest_dir.as_os_str().to_str().unwrap(), "--destination", app_package_name.as_os_str().to_str().unwrap(), "--target-api-set", &package_config.arv, ]; let app_package = cargo_metadata["packages"] .as_array() .unwrap() .into_iter() .find(|&x| x["name"] == package_config.name); let hw_defs_path = if let Some(target_hardware) = &package_config.target_hardware { if self.verbose { println!("target_hardware {:?}\n", target_hardware); println!("app_package: {:?}\n", app_package); } if let Some(app_package) = app_package { let hardware = app_package["dependencies"] .as_array() .unwrap() .into_iter() .find(|&x| x["name"] == "hardware"); if let Some(hardware) = hardware { let path = &hardware["path"].to_string().replace("\"", ""); Some( PathBuf::from(path) .join("HardwareDefinitions") .join(target_hardware), ) } else { None } } else { None } } else { None }; let sdk_hw_definitions = sdk_path.join("HardwareDefinitions"); if let Some(hw_defs_path) = &hw_defs_path { if let Some(target_definition_file) = &target_definition_file { args.push("--target-definition-filename"); args.push(target_definition_file.as_str()); } if package_config.target_definition.is_some() { args.push("--hardware-definitions"); args.push(hw_defs_path.as_os_str().to_str().unwrap()); args.push(sdk_hw_definitions.as_os_str().to_str().unwrap()); } } if self.verbose { args.push("--verbose"); println!("Args: {:?}\n", args); } println!( "Generating {}", app_package_name.as_os_str().to_str().unwrap() ); let output = Command::new(azsphere) .args(args) .output() .expect("Failed to create app package"); if !output.status.success() { println!("azsphere command failed:"); io::stdout().write_all(&output.stdout).unwrap(); io::stderr().write_all(&output.stderr).unwrap(); std::process::exit(output.status.code().unwrap()); } Ok(()) } }