cargo_lambda_build/
lib.rs

1use cargo_lambda_interactive::{error::InquireError, is_user_cancellation_error};
2use cargo_lambda_metadata::{
3    cargo::{
4        CargoMetadata, binary_targets_from_metadata,
5        build::{Build, OutputFormat},
6        cargo_release_profile_config, target_dir_from_metadata,
7    },
8    fs::copy_and_replace,
9};
10use miette::{IntoDiagnostic, Report, Result, WrapErr};
11use std::{
12    collections::HashSet,
13    fs::create_dir_all,
14    path::{Path, PathBuf},
15    str::FromStr,
16};
17use target_arch::TargetArch;
18use tracing::{debug, warn};
19
20pub use cargo_zigbuild::Zig;
21
22mod archive;
23pub use archive::{BinaryArchive, BinaryData, BinaryModifiedAt, create_binary_archive, zip_binary};
24
25mod compiler;
26use compiler::{build_command, build_profile};
27
28mod error;
29use error::BuildError;
30
31mod target_arch;
32use target_arch::validate_linux_target;
33
34mod toolchain;
35use toolchain::rustup_cmd;
36
37pub mod zig;
38
39#[tracing::instrument(skip(build, metadata), target = "cargo_lambda")]
40pub async fn run(build: &mut Build, metadata: &CargoMetadata) -> Result<()> {
41    tracing::trace!(options = ?build, "building project");
42
43    if (build.arm64 || build.x86_64) && !build.cargo_opts.target.is_empty() {
44        Err(BuildError::InvalidTargetOptions)?;
45    }
46
47    let target_arch = if build.arm64 {
48        TargetArch::arm64()
49    } else if build.x86_64 {
50        TargetArch::x86_64()
51    } else {
52        // let build_target = build.cargo_opts.target.first().or(metadata.target.as_ref());
53        match build.cargo_opts.target.first() {
54            Some(target) => {
55                validate_linux_target(target)?;
56                TargetArch::from_str(target)?
57            }
58            None => TargetArch::from_host()?,
59        }
60    };
61
62    build.cargo_opts.target = vec![target_arch.to_string()];
63
64    let build_examples = build.cargo_opts.examples || !build.cargo_opts.example.is_empty();
65    let binaries = binary_targets_from_metadata(metadata, build_examples);
66    debug!(binaries = ?binaries, "found new target binaries to build");
67
68    let binaries = if !build.cargo_opts.bin.is_empty() {
69        let mut final_binaries = HashSet::with_capacity(binaries.len());
70
71        for name in &build.cargo_opts.bin {
72            if !binaries.contains(name) {
73                return Err(BuildError::FunctionBinaryMissing(name.into()).into());
74            }
75            final_binaries.insert(name.into());
76        }
77
78        final_binaries
79    } else {
80        binaries
81    };
82
83    let compiler_option = build.compiler.clone().unwrap_or_default();
84    if compiler_option.is_local_cargo() {
85        // This check only makes sense when the build host is local.
86        // If the build host was ever going to be remote, like in a container,
87        // this is not checked
88        if !target_arch.compatible_host_linker() && !target_arch.is_static_linking() {
89            return Err(BuildError::InvalidCompilerOption.into());
90        }
91    }
92
93    if is_release_profile(build) && !build.disable_optimizations {
94        let release_optimizations =
95            cargo_release_profile_config(metadata).map_err(BuildError::MetadataError)?;
96        build.cargo_opts.config.extend(
97            release_optimizations
98                .into_iter()
99                .map(String::from)
100                .collect::<Vec<_>>(),
101        );
102
103        let build_flags = format!(
104            "build.rustflags=[\"-C\", \"target-cpu={}\"]",
105            target_arch.target_cpu()
106        );
107        build.cargo_opts.config.push(build_flags);
108
109        debug!(config = ?build.cargo_opts.config, "release optimizations");
110    }
111
112    let profile = build_profile(&build.cargo_opts, &compiler_option);
113    let skip_target_check = build.skip_target_check || which::which(rustup_cmd()).is_err();
114    let cmd = build_command(
115        &compiler_option,
116        &build.cargo_opts,
117        &target_arch,
118        metadata,
119        skip_target_check,
120    )
121    .await;
122
123    let mut cmd = match cmd {
124        Ok(cmd) => cmd,
125        Err(err) if downcasted_user_cancellation(&err) => return Ok(()),
126        Err(err) => return Err(err),
127    };
128
129    let mut child = cmd.spawn().map_err(BuildError::FailedBuildCommand)?;
130    let status = child.wait().map_err(BuildError::FailedBuildCommand)?;
131    if !status.success() {
132        std::process::exit(status.code().unwrap_or(1));
133    }
134
135    // extract resolved target dir from cargo metadata
136    let target_dir = target_dir_from_metadata(metadata).unwrap_or_else(|_| PathBuf::from("target"));
137    let target_dir = Path::new(&target_dir);
138    let lambda_dir = if let Some(dir) = &build.lambda_dir {
139        dir.clone()
140    } else {
141        target_dir.join("lambda")
142    };
143
144    let mut base = target_dir
145        .join(target_arch.rustc_target_without_glibc_version())
146        .join(profile);
147    if build_examples {
148        base = base.join("examples");
149    }
150
151    let mut found_binaries = false;
152    for name in &binaries {
153        let binary = base.join(name);
154        debug!(binary = ?binary, exists = binary.exists(), "checking function binary");
155
156        if binary.exists() {
157            found_binaries = true;
158
159            let bootstrap_dir = if build.extension {
160                lambda_dir.join("extensions")
161            } else {
162                match build.flatten {
163                    Some(ref n) if n == name => lambda_dir.clone(),
164                    _ => lambda_dir.join(name),
165                }
166            };
167            create_dir_all(&bootstrap_dir)
168                .into_diagnostic()
169                .wrap_err_with(|| format!("error creating lambda directory {bootstrap_dir:?}"))?;
170
171            let data = BinaryData::new(name.as_str(), build.extension, build.internal);
172
173            match build.output_format() {
174                OutputFormat::Binary => {
175                    let output_location = bootstrap_dir.join(data.binary_name());
176                    copy_and_replace(&binary, &output_location)
177                        .into_diagnostic()
178                        .wrap_err_with(|| {
179                            format!("error moving the binary `{binary:?}` into the output location `{output_location:?}`")
180                        })?;
181                }
182                OutputFormat::Zip => {
183                    zip_binary(binary, bootstrap_dir, &data, build.include.clone())?;
184                }
185            }
186        }
187    }
188    if !found_binaries {
189        warn!(
190            ?base,
191            "no binaries found in target directory after build, try using the --bin, --example, or --package options to build specific binaries"
192        );
193    }
194
195    Ok(())
196}
197
198fn downcasted_user_cancellation(err: &Report) -> bool {
199    match err.root_cause().downcast_ref::<InquireError>() {
200        Some(err) => is_user_cancellation_error(err),
201        None => false,
202    }
203}
204
205fn is_release_profile(build: &Build) -> bool {
206    build.cargo_opts.release
207        || build
208            .cargo_opts
209            .profile
210            .as_deref()
211            .is_some_and(|p| p == "release")
212}