Skip to main content

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    // Apply build environment variables
130    if let Ok(env_vars) = build.build_environment() {
131        for (key, value) in env_vars {
132            cmd.env(key, value);
133        }
134    }
135
136    let mut child = cmd.spawn().map_err(BuildError::FailedBuildCommand)?;
137    let status = child.wait().map_err(BuildError::FailedBuildCommand)?;
138    if !status.success() {
139        std::process::exit(status.code().unwrap_or(1));
140    }
141
142    // extract resolved target dir from cargo metadata
143    let target_dir = target_dir_from_metadata(metadata).unwrap_or_else(|_| PathBuf::from("target"));
144    let target_dir = Path::new(&target_dir);
145    let lambda_dir = if let Some(dir) = &build.lambda_dir {
146        dir.clone()
147    } else {
148        target_dir.join("lambda")
149    };
150
151    let mut base = target_dir
152        .join(target_arch.rustc_target_without_glibc_version())
153        .join(profile);
154    if build_examples {
155        base = base.join("examples");
156    }
157
158    let mut found_binaries = false;
159    for name in &binaries {
160        let binary = base.join(name);
161        debug!(binary = ?binary, exists = binary.exists(), "checking function binary");
162
163        if binary.exists() {
164            found_binaries = true;
165
166            let bootstrap_dir = if build.extension {
167                lambda_dir.join("extensions")
168            } else {
169                match build.flatten {
170                    Some(ref n) if n == name => lambda_dir.clone(),
171                    _ => lambda_dir.join(name),
172                }
173            };
174            create_dir_all(&bootstrap_dir)
175                .into_diagnostic()
176                .wrap_err_with(|| format!("error creating lambda directory {bootstrap_dir:?}"))?;
177
178            let data = BinaryData::new(name.as_str(), build.extension, build.internal);
179
180            match build.output_format() {
181                OutputFormat::Binary => {
182                    let output_location = bootstrap_dir.join(data.binary_name());
183                    copy_and_replace(&binary, &output_location)
184                        .into_diagnostic()
185                        .wrap_err_with(|| {
186                            format!("error moving the binary `{binary:?}` into the output location `{output_location:?}`")
187                        })?;
188                }
189                OutputFormat::Zip => {
190                    zip_binary(binary, bootstrap_dir, &data, build.include.clone())?;
191                }
192            }
193        }
194    }
195    if !found_binaries {
196        warn!(
197            ?base,
198            "no binaries found in target directory after build, try using the --bin, --example, or --package options to build specific binaries"
199        );
200    }
201
202    Ok(())
203}
204
205fn downcasted_user_cancellation(err: &Report) -> bool {
206    match err.root_cause().downcast_ref::<InquireError>() {
207        Some(err) => is_user_cancellation_error(err),
208        None => false,
209    }
210}
211
212fn is_release_profile(build: &Build) -> bool {
213    build.cargo_opts.release
214        || build
215            .cargo_opts
216            .profile
217            .as_deref()
218            .is_some_and(|p| p == "release")
219}