cargo_npk/
lib.rs

1use anyhow::{anyhow, bail, Context};
2use cargo_metadata::MetadataCommand;
3use cli::{ColorChoice, Compression};
4use human_bytes::human_bytes;
5use humantime::format_duration;
6use std::{
7    env::current_dir,
8    ffi::OsString,
9    fs, io,
10    io::Write,
11    path::PathBuf,
12    process::{self},
13    time,
14};
15use termcolor::{Color, ColorSpec, StandardStream, WriteColor};
16
17use std::path::Path;
18
19use anyhow::Result;
20use cargo_subcommand::{Profile, Subcommand};
21use clap::Parser;
22use northstar_runtime::npk::npk::{NpkBuilder, SquashfsOptions};
23
24use crate::metadata::Metadata;
25
26use std::io::IsTerminal;
27
28mod cli;
29mod metadata;
30
31const CROSS: &str = "cross";
32const CARGO: &str = "cargo";
33const MKSQUASHFS: &str = "mksquashfs";
34
35pub fn npk<I, T>(args: I) -> Result<()>
36where
37    I: IntoIterator<Item = T>,
38    T: Into<OsString> + Clone + std::fmt::Debug + ToString,
39{
40    let cli::Command {
41        npk: cli::NpkCommand::Npk { cmd },
42    } = cli::Command::parse_from(args);
43
44    match cmd {
45        cli::NpkSubCommand::Pack {
46            args,
47            key,
48            compression,
49            block_size,
50            mksquashfs,
51            clones,
52            color,
53            out,
54        } => {
55            let cmd = Subcommand::new(args.subcommand_args)?;
56            pack(
57                cmd,
58                key.as_deref(),
59                compression,
60                block_size,
61                mksquashfs,
62                clones,
63                color,
64                out.as_deref(),
65            )
66        }
67        cli::NpkSubCommand::Version => {
68            println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
69            Ok(())
70        }
71    }
72}
73#[allow(clippy::too_many_arguments)]
74fn pack(
75    cmd: Subcommand,
76    key: Option<&Path>,
77    compression: Compression,
78    block_size: Option<u32>,
79    mksquashfs: Option<PathBuf>,
80    clones: Option<u32>,
81    color: ColorChoice,
82    out_dir: Option<&Path>,
83) -> Result<()> {
84    let start = time::Instant::now();
85    let mut stdout = stdout(color);
86    let quiet = cmd.quiet();
87    let mut log = move |tag: &str, msg: &str| -> Result<()> {
88        if !quiet {
89            stdout.set_color(ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)))?;
90            write!(stdout, "{tag:>12} ")?;
91            stdout.reset()?;
92            writeln!(stdout, "{msg}")?;
93        }
94        Ok(())
95    };
96
97    let target = cmd.target().unwrap_or_else(|| cmd.host_triple());
98
99    if cmd.artifacts().count() > 1 {
100        bail!("crates with multiple artifacts are unsupported");
101    }
102
103    // TODO: feature support.
104    let cargo_metadata = MetadataCommand::new()
105        .manifest_path(cmd.manifest())
106        .exec()?;
107
108    // Parse package.
109    let package = cargo_metadata
110        .packages
111        .iter()
112        .find(|p| p.manifest_path == cmd.manifest())
113        .ok_or_else(|| anyhow!("failed to find package"))?;
114
115    // Northstar manifest and metadata.
116    let metadata = Metadata::deserilize(cmd.manifest(), &package.metadata, target)?;
117    let northstar_manifest = &metadata.manifest;
118
119    // Build.
120    log(
121        "Building",
122        &format!(
123            "{} from {}{}",
124            package.name,
125            cmd.manifest().display(),
126            cmd.target().map(|t| format! {" [{t}]"}).unwrap_or_default()
127        ),
128    )?;
129    let executable = build(&cmd, target, metadata.use_cross)?;
130
131    // Rootfs.
132    let tempdir = tempfile::TempDir::new().context("failed to create tempdir")?;
133    let root = tempdir.path().to_owned();
134    rootfs(&root, &metadata, &executable)?;
135
136    // Pack.
137    let squashfs_opts = SquashfsOptions {
138        compression: compression.into(),
139        block_size,
140        mksquashfs: mksquashfs.unwrap_or_else(|| PathBuf::from(MKSQUASHFS)),
141    };
142
143    // If the target matches the host tripple there's not triple subdir in target.
144    let target = if cmd.target() == Some(cmd.host_triple()) {
145        None
146    } else {
147        cmd.target()
148    };
149
150    // Place the npk in `out_dir` if proficed - otherwise calculate the build dir.
151    let out = if let Some(out_dir) = out_dir {
152        if !out_dir.is_dir() {
153            fs::create_dir_all(out_dir)
154                .with_context(|| format!("failed to create {}", out_dir.display()))?;
155        }
156        out_dir.to_owned()
157    } else {
158        cmd.build_dir(target)
159    };
160
161    let builder = NpkBuilder::default().root(&root, Some(&squashfs_opts));
162    let builder = if let Some(key) = key {
163        builder.key(key)
164    } else {
165        builder
166    };
167
168    if let Some(clones) = clones {
169        let name = northstar_manifest.name.clone();
170        let num = clones.to_string().chars().count();
171        let mut manifest = northstar_manifest.clone();
172        for n in 0..clones {
173            manifest.name = format!("{name}-{n:0num$}")
174                .try_into()
175                .context("failed to parse name")?;
176            let (npk, npk_size) = builder.clone().manifest(&manifest).to_dir(&out)?;
177            let npk_size = human_bytes(npk_size as f64);
178            let msg = format!("{} [{}, {}]", npk.display(), npk_size, compression);
179            log("Packed", &msg)?;
180        }
181        let duration = format_duration(time::Duration::from_secs(start.elapsed().as_secs()));
182        log("Finished", &format!("{clones} clones in {duration}"))?;
183    } else {
184        let (npk, npk_size) = builder.manifest(northstar_manifest).to_dir(&out)?;
185        let npk_size = human_bytes(npk_size as f64);
186        let duration = format_duration(time::Duration::from_secs(start.elapsed().as_secs()));
187        let msg = format!(
188            "{} [{}, {}] in {}",
189            npk.display(),
190            npk_size,
191            compression,
192            duration
193        );
194        log("Packed", &msg)?;
195    }
196
197    Ok(())
198}
199
200fn build(subcommand: &Subcommand, target: &str, use_cross: bool) -> Result<PathBuf> {
201    // Select "cargo" and manifest path
202    let (cargo, cargo_manifest) = if use_cross {
203        (
204            CROSS,
205            // Cross requires a relative dir in order to map pathes
206            // into a container.
207            subcommand.manifest().strip_prefix(current_dir()?)?,
208        )
209    } else {
210        (CARGO, subcommand.manifest())
211    };
212
213    let mut command = process::Command::new(cargo);
214
215    command.arg("build");
216
217    let manifest_path = cargo_manifest.display().to_string();
218    command.args(["--manifest-path", &manifest_path]);
219
220    let target_dir = subcommand.target_dir().display().to_string();
221    command.args(["--target-dir", &target_dir]);
222
223    if subcommand.quiet() {
224        command.args(["--quiet", target]);
225    }
226    if target != subcommand.host_triple() {
227        command.args(["--target", target]);
228    }
229    if subcommand.profile() == &Profile::Release {
230        command.arg("--release");
231    }
232    // Spawn cargo/cross and wait for the binary to be finished.
233    if !command
234        .spawn()
235        .context("failed to spawn cargo/cross")?
236        .wait()?
237        .success()
238    {
239        bail!("failed to run cargo");
240    }
241
242    let artifact = subcommand.artifacts().next().expect("missing artifact");
243    let executable = subcommand.artifact(
244        artifact,
245        if subcommand.target() == Some(subcommand.host_triple()) {
246            None
247        } else {
248            subcommand.target()
249        },
250        cargo_subcommand::CrateType::Bin,
251    );
252
253    Ok(executable)
254}
255
256fn rootfs(root: &Path, metadata: &Metadata, executable: &Path) -> Result<()> {
257    // Calculate root. If the root is not set in the cargo manifest create and use an empty tempdir.
258    if let Some(metadata_root) = &metadata.root {
259        copy_dir_all(root, metadata_root).context("failed to copy root")?;
260    }
261
262    // Extract init from the northstar manifest.
263    let init: &Path = metadata
264        .manifest
265        .init
266        .as_ref()
267        .ok_or_else(|| anyhow!("resource containers are unsupported"))?
268        .as_ref();
269    let init_in_rootfs = root.join(init.strip_prefix("/").unwrap_or(init));
270
271    // Check if init is a file in the rootfs provided.
272    if init_in_rootfs.is_file() {
273        bail!("failed create root fs. root contains {}", init.display());
274    }
275    // Check if init is a directory in the rootfs provided.
276    if init_in_rootfs.is_dir() {
277        bail!("failed create root fs. {} is a directoy", init.display());
278    }
279
280    // Create the parent dir of init in the tmp rootfs.
281    if let Some(parent) = init_in_rootfs.parent() {
282        debug_assert!(!parent.is_file());
283        if !parent.is_dir() {
284            fs::create_dir_all(parent).context("failed to create directory")?;
285        }
286    }
287
288    fs::copy(executable, init_in_rootfs).context("failed to copy artifact")?;
289
290    Ok(())
291}
292
293fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
294    fs::create_dir_all(&dst)?;
295    for entry in fs::read_dir(src)? {
296        let entry = entry?;
297        let ty = entry.file_type()?;
298        if ty.is_dir() {
299            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
300        } else {
301            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
302        }
303    }
304    Ok(())
305}
306
307fn stdout(choice: ColorChoice) -> StandardStream {
308    let choice = match choice {
309        ColorChoice::Always => termcolor::ColorChoice::Always,
310        ColorChoice::Never => termcolor::ColorChoice::AlwaysAnsi,
311        ColorChoice::Auto => {
312            if std::io::stdout().is_terminal() {
313                termcolor::ColorChoice::Auto
314            } else {
315                termcolor::ColorChoice::Never
316            }
317        }
318    };
319    StandardStream::stdout(choice)
320}