cargo_lambda_build/
lib.rs1use 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 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 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 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}