Skip to main content

bern_target_api/
plan.rs

1//! Build plans: the steps a target asks the build driver to run, how
2//! `bern run` starts the product, and helpers that make steps and place
3//! packages.
4
5use crate::BuildContext;
6use std::path::PathBuf;
7
8/// One step the build driver executes for a target.
9#[derive(Clone, Debug)]
10pub enum BuildStep {
11    Run {
12        description: String,
13        program: String,
14        args: Vec<String>,
15        cwd: PathBuf,
16        env: Vec<(String, String)>,
17    },
18    CreateDir {
19        path: PathBuf,
20    },
21    Copy {
22        from: PathBuf,
23        to: PathBuf,
24    },
25    RemoveIfExists {
26        path: PathBuf,
27    },
28    /// Write a file, creating its directory.
29    Write {
30        path: PathBuf,
31        contents: Vec<u8>,
32    },
33    /// Make `dir` hold exactly `files` plus the paths in `keep`, which
34    /// other steps own (for example generated localizations). Files are
35    /// copied by content, without extended attributes; a destination whose
36    /// size and modification time match its source is left alone. Anything
37    /// else below `dir` is removed, so files dropped from a declaration do
38    /// not linger in the product.
39    Mirror {
40        dir: PathBuf,
41        /// Source file and destination relative to `dir`.
42        files: Vec<(PathBuf, PathBuf)>,
43        /// Paths relative to `dir` to leave in place.
44        keep: Vec<PathBuf>,
45    },
46}
47
48#[derive(Clone, Debug, Default)]
49pub struct BuildPlan {
50    pub steps: Vec<BuildStep>,
51    /// Final artifacts, for reporting.
52    pub products: Vec<PathBuf>,
53    /// How `bern run` starts the built application.
54    pub run: Option<Run>,
55    /// What the person building should know about the products, printed
56    /// after the steps; for example that a package is not signed.
57    pub notes: Vec<String>,
58}
59
60/// How `bern run` starts a built application.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub enum Run {
63    /// Start the program and wait until it exits.
64    Program(PathBuf),
65    /// Serve the directory on the loopback interface and open its
66    /// `index.html` in the browser.
67    Page(PathBuf),
68    /// Start the program, which serves its own page and prints the address
69    /// on a line `open <address>`; open that address in the browser and
70    /// wait until the program exits.
71    PageHost(PathBuf),
72    /// Install the app bundle into a booted simulator of `system`, start it
73    /// there and wait until it exits.
74    Simulator {
75        app: PathBuf,
76        bundle_id: String,
77        system: SimulatedSystem,
78    },
79    /// Install the APK on the Android device `adb` reaches, start its
80    /// activity and show its log until it exits.
81    Android {
82        apk: PathBuf,
83        package: String,
84        adb: PathBuf,
85        /// The socket on this computer and the app's abstract socket that
86        /// `adb forward` joins, for the control channel.
87        forward: Option<(PathBuf, String)>,
88    },
89}
90
91/// The system a simulator runs, as its runtimes name it.
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum SimulatedSystem {
94    Ios,
95    Tvos,
96}
97
98impl SimulatedSystem {
99    /// The system's name in simulator runtimes and in messages: `iOS`,
100    /// `tvOS`.
101    pub fn name(self) -> &'static str {
102        match self {
103            SimulatedSystem::Ios => "iOS",
104            SimulatedSystem::Tvos => "tvOS",
105        }
106    }
107}
108
109/// Where packages go: `target/bern/dist/` of the workspace.
110pub fn dist_dir(context: &BuildContext) -> PathBuf {
111    context
112        .workspace_dir
113        .join("target")
114        .join("bern")
115        .join("dist")
116}
117
118/// The build machine's architecture as packages name it: `arm64` or
119/// `x64` (`x86_64` on macOS, where that is the usual name).
120pub fn package_architecture() -> &'static str {
121    match (std::env::consts::OS, std::env::consts::ARCH) {
122        (_, "aarch64") => "arm64",
123        ("macos", _) => "x86_64",
124        _ => "x64",
125    }
126}
127
128/// A step that archives `dir`, with `dir` itself as the top folder, into
129/// `stem` plus the extension the build machine's own tools make: `.zip`
130/// on macOS (`ditto`) and Windows (`tar`), `.tar.gz` on Linux (`tar`).
131pub fn archive(dir: &std::path::Path, stem: &std::path::Path) -> (BuildStep, PathBuf) {
132    let parent = dir.parent().unwrap_or(dir).to_path_buf();
133    let name = dir
134        .file_name()
135        .map(|name| name.to_string_lossy().into_owned())
136        .unwrap_or_default();
137    let with = |extension: &str| {
138        let mut file = stem.as_os_str().to_owned();
139        file.push(extension);
140        PathBuf::from(file)
141    };
142    let (program, args, out) = match std::env::consts::OS {
143        "macos" => {
144            let out = with(".zip");
145            let args = vec![
146                "-c".to_string(),
147                "-k".into(),
148                // Plain files: no resource forks, extended attributes or
149                // quarantine flags (`__MACOSX/._*` entries elsewhere).
150                "--norsrc".into(),
151                "--noextattr".into(),
152                "--noqtn".into(),
153                "--keepParent".into(),
154                dir.display().to_string(),
155                out.display().to_string(),
156            ];
157            ("ditto", args, out)
158        }
159        "windows" => {
160            let out = with(".zip");
161            let args = vec![
162                "-a".to_string(),
163                "-c".into(),
164                "-f".into(),
165                out.display().to_string(),
166                "-C".into(),
167                parent.display().to_string(),
168                name.clone(),
169            ];
170            ("tar", args, out)
171        }
172        _ => {
173            let out = with(".tar.gz");
174            let args = vec![
175                "-czf".to_string(),
176                out.display().to_string(),
177                "-C".into(),
178                parent.display().to_string(),
179                name.clone(),
180            ];
181            ("tar", args, out)
182        }
183    };
184    let step = BuildStep::Run {
185        description: format!("Archive {name}"),
186        program: program.into(),
187        args,
188        cwd: parent,
189        env: vec![],
190    };
191    (step, out)
192}
193
194/// Every file below `dir`, as (path, path relative to `dir`), sorted.
195pub fn files_below(dir: &std::path::Path) -> std::io::Result<Vec<(PathBuf, PathBuf)>> {
196    fn walk(
197        dir: &std::path::Path,
198        relative: &std::path::Path,
199        out: &mut Vec<(PathBuf, PathBuf)>,
200    ) -> std::io::Result<()> {
201        for entry in std::fs::read_dir(dir.join(relative))? {
202            let entry = entry?;
203            let path = relative.join(entry.file_name());
204            if entry.file_type()?.is_dir() {
205                walk(dir, &path, out)?;
206            } else {
207                out.push((dir.join(&path), path));
208            }
209        }
210        Ok(())
211    }
212    let mut out = Vec::new();
213    walk(dir, std::path::Path::new(""), &mut out)?;
214    out.sort();
215    Ok(out)
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn archives_use_the_build_machine_s_own_tools() {
224        let (step, out) = archive(
225            std::path::Path::new("/stage/Hello"),
226            std::path::Path::new("/dist/Hello-1.0"),
227        );
228        let BuildStep::Run { program, args, .. } = step else {
229            panic!("a command");
230        };
231        match std::env::consts::OS {
232            "macos" => {
233                assert_eq!(
234                    (program.as_str(), out),
235                    ("ditto", PathBuf::from("/dist/Hello-1.0.zip"))
236                );
237                assert!(args.contains(&"--noextattr".to_string()));
238            }
239            "windows" => assert_eq!(program, "tar"),
240            _ => assert_eq!(
241                (program.as_str(), out),
242                ("tar", PathBuf::from("/dist/Hello-1.0.tar.gz"))
243            ),
244        }
245    }
246}