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 let cargo_metadata = MetadataCommand::new()
105 .manifest_path(cmd.manifest())
106 .exec()?;
107
108 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 let metadata = Metadata::deserilize(cmd.manifest(), &package.metadata, target)?;
117 let northstar_manifest = &metadata.manifest;
118
119 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 let tempdir = tempfile::TempDir::new().context("failed to create tempdir")?;
133 let root = tempdir.path().to_owned();
134 rootfs(&root, &metadata, &executable)?;
135
136 let squashfs_opts = SquashfsOptions {
138 compression: compression.into(),
139 block_size,
140 mksquashfs: mksquashfs.unwrap_or_else(|| PathBuf::from(MKSQUASHFS)),
141 };
142
143 let target = if cmd.target() == Some(cmd.host_triple()) {
145 None
146 } else {
147 cmd.target()
148 };
149
150 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 let (cargo, cargo_manifest) = if use_cross {
203 (
204 CROSS,
205 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 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 if let Some(metadata_root) = &metadata.root {
259 copy_dir_all(root, metadata_root).context("failed to copy root")?;
260 }
261
262 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 if init_in_rootfs.is_file() {
273 bail!("failed create root fs. root contains {}", init.display());
274 }
275 if init_in_rootfs.is_dir() {
277 bail!("failed create root fs. {} is a directoy", init.display());
278 }
279
280 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}