Skip to main content

cargo_zigbuild/
build.rs

1use std::ops::{Deref, DerefMut};
2use std::path::PathBuf;
3#[cfg(feature = "universal2")]
4use std::process::Stdio;
5use std::process::{self, Child, Command};
6
7use anyhow::{Context, Result};
8use clap::Parser;
9
10use crate::zig::Zig;
11
12/// Compile a local package and all of its dependencies
13/// using zig as the linker
14#[derive(Clone, Debug, Default, Parser)]
15#[command(
16    after_help = "Run `cargo help build` for more detailed information.",
17    display_order = 1
18)]
19pub struct Build {
20    #[command(flatten)]
21    pub cargo: cargo_options::Build,
22
23    /// Disable zig linker
24    #[arg(skip)]
25    pub disable_zig_linker: bool,
26
27    /// Enable zig ar
28    #[arg(skip)]
29    pub enable_zig_ar: bool,
30
31    /// Minimum macOS version, overrides `MACOSX_DEPLOYMENT_TARGET`
32    #[arg(skip)]
33    pub macosx_deployment_target: Option<String>,
34}
35
36impl Build {
37    /// Create a new build from manifest path
38    #[allow(clippy::field_reassign_with_default)]
39    pub fn new(manifest_path: Option<PathBuf>) -> Self {
40        let mut build = Self::default();
41        build.manifest_path = manifest_path;
42        build
43    }
44
45    /// Execute `cargo build` command with zig as the linker
46    pub fn execute(&self) -> Result<()> {
47        let has_universal2 = self
48            .cargo
49            .target
50            .contains(&"universal2-apple-darwin".to_string());
51        let mut build = self.build_command()?;
52        let mut child = build.spawn().context("Failed to run cargo build")?;
53        if has_universal2 {
54            self.handle_universal2_build(child)?;
55        } else {
56            let status = child.wait().expect("Failed to wait on cargo build process");
57            if !status.success() {
58                process::exit(status.code().unwrap_or(1));
59            }
60        }
61        Ok(())
62    }
63
64    #[cfg(not(feature = "universal2"))]
65    fn handle_universal2_build(&self, mut _child: Child) -> Result<()> {
66        anyhow::bail!("Unsupported Rust target: universal2-apple-darwin")
67    }
68
69    #[cfg(feature = "universal2")]
70    fn handle_universal2_build(&self, mut child: Child) -> Result<()> {
71        use cargo_metadata::Message;
72        use std::io::BufReader;
73        use std::path::Path;
74
75        // Find workspace member package ids
76        let manifest_path = self
77            .manifest_path
78            .as_deref()
79            .unwrap_or_else(|| Path::new("Cargo.toml"));
80        let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
81        metadata_cmd.manifest_path(manifest_path);
82        let metadata = metadata_cmd.exec()?;
83        let member_ids: std::collections::HashSet<_> = metadata.workspace_members.iter().collect();
84
85        let mut x86_64_artifacts = Vec::new();
86        let mut aarch64_artifacts = Vec::new();
87
88        let stream = child
89            .stdout
90            .take()
91            .expect("Cargo build should have a stdout");
92        for message in Message::parse_stream(BufReader::new(stream)) {
93            let message = message.context("Failed to parse cargo metadata message")?;
94            match message {
95                Message::CompilerArtifact(artifact) => {
96                    if member_ids.contains(&artifact.package_id) {
97                        for filename in artifact.filenames {
98                            if filename.as_str().contains("x86_64-apple-darwin") {
99                                x86_64_artifacts.push(filename);
100                            } else if filename.as_str().contains("aarch64-apple-darwin") {
101                                aarch64_artifacts.push(filename);
102                            }
103                        }
104                    }
105                }
106                Message::CompilerMessage(msg) => {
107                    println!("{}", msg.message);
108                }
109                _ => {}
110            }
111        }
112        let status = child.wait().expect("Failed to wait on cargo build process");
113        if !status.success() {
114            process::exit(status.code().unwrap_or(1));
115        }
116        // create fat binaries for artifacts
117        for (x86_64_path, aarch64_path) in x86_64_artifacts
118            .into_iter()
119            .zip(aarch64_artifacts.into_iter())
120        {
121            let mut fat = fat_macho::FatWriter::new();
122            match fat.add(fs_err::read(&x86_64_path)?) {
123                Err(fat_macho::Error::InvalidMachO(_)) => continue,
124                Err(e) => return Err(e)?,
125                Ok(()) => {}
126            }
127            match fat.add(fs_err::read(&aarch64_path)?) {
128                Err(fat_macho::Error::InvalidMachO(_)) => continue,
129                Err(e) => return Err(e)?,
130                Ok(()) => {}
131            }
132            let universal2_path = PathBuf::from(
133                x86_64_path
134                    .to_string()
135                    .replace("x86_64-apple-darwin", "universal2-apple-darwin"),
136            );
137            let universal2_dir = universal2_path.parent().unwrap();
138            fs_err::create_dir_all(universal2_dir)?;
139            fat.write_to_file(universal2_path)?;
140        }
141        Ok(())
142    }
143
144    /// Generate cargo subcommand
145    #[cfg(not(feature = "universal2"))]
146    pub fn build_command(&self) -> Result<Command> {
147        let mut build = self.cargo.command();
148        if !self.disable_zig_linker {
149            Zig::apply_command_env(
150                self.manifest_path.as_deref(),
151                self.release,
152                &self.cargo.common,
153                &mut build,
154                self.enable_zig_ar,
155                self.macosx_deployment_target.as_deref(),
156            )?;
157        }
158        Ok(build)
159    }
160
161    /// Generate cargo subcommand
162    #[cfg(feature = "universal2")]
163    pub fn build_command(&self) -> Result<Command> {
164        let build = if let Some(index) = self
165            .cargo
166            .target
167            .iter()
168            .position(|t| t == "universal2-apple-darwin")
169        {
170            let mut cargo = self.cargo.clone();
171            cargo.target.remove(index);
172            if !cargo.target.contains(&"x86_64-apple-darwin".to_string()) {
173                cargo.target.push("x86_64-apple-darwin".to_string());
174            }
175            if !cargo.target.contains(&"aarch64-apple-darwin".to_string()) {
176                cargo.target.push("aarch64-apple-darwin".to_string());
177            }
178            if !cargo.message_format.iter().any(|f| f.starts_with("json")) {
179                cargo.message_format.push("json".to_string());
180            }
181            let mut build = cargo.command();
182            build.stdout(Stdio::piped()).stderr(Stdio::inherit());
183            if !self.disable_zig_linker {
184                Zig::apply_command_env(
185                    self.manifest_path.as_deref(),
186                    self.release,
187                    &cargo.common,
188                    &mut build,
189                    self.enable_zig_ar,
190                    self.macosx_deployment_target.as_deref(),
191                )?;
192            }
193            build
194        } else {
195            let mut build = self.cargo.command();
196            if !self.disable_zig_linker {
197                Zig::apply_command_env(
198                    self.manifest_path.as_deref(),
199                    self.release,
200                    &self.cargo.common,
201                    &mut build,
202                    self.enable_zig_ar,
203                    self.macosx_deployment_target.as_deref(),
204                )?;
205            }
206            build
207        };
208        Ok(build)
209    }
210}
211
212impl Deref for Build {
213    type Target = cargo_options::Build;
214
215    fn deref(&self) -> &Self::Target {
216        &self.cargo
217    }
218}
219
220impl DerefMut for Build {
221    fn deref_mut(&mut self) -> &mut Self::Target {
222        &mut self.cargo
223    }
224}
225
226impl From<cargo_options::Build> for Build {
227    fn from(cargo: cargo_options::Build) -> Self {
228        Self {
229            cargo,
230            ..Default::default()
231        }
232    }
233}