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