vorpal_sdk/artifact/language/
rust.rs1use crate::{
2 api::artifact::{ArtifactStepSecret, ArtifactSystem},
3 artifact::{
4 get_env_key, protoc::Protoc, rust_toolchain, rust_toolchain::RustToolchain, step, Artifact,
5 ArtifactSource, DevelopmentEnvironment,
6 },
7 context::ConfigContext,
8};
9use anyhow::{bail, Result};
10use indoc::formatdoc;
11use serde::Deserialize;
12use std::fs::read_to_string;
13use toml::from_str;
14
15#[derive(Debug, Deserialize)]
16struct RustCargoToml {
17 bin: Option<Vec<RustCargoTomlBinary>>,
18 package: Option<RustCargoTomlPackage>,
19 workspace: Option<RustCargoTomlWorkspace>,
20}
21
22#[derive(Debug, Deserialize)]
23struct RustCargoTomlBinary {
24 name: String,
25 path: String,
26}
27
28#[derive(Debug, Deserialize)]
29struct RustCargoTomlPackage {
30 name: String,
31 }
33
34#[derive(Debug, Deserialize)]
35struct RustCargoTomlWorkspace {
36 members: Option<Vec<String>>,
37}
38
39pub struct Rust<'a> {
40 artifacts: Vec<String>,
41 bins: Vec<String>,
42 build: bool,
43 check: bool,
44 environments: Vec<&'a str>,
45 excludes: Vec<&'a str>,
46 format: bool,
47 includes: Vec<&'a str>,
48 lint: bool,
49 name: &'a str,
50 packages: Vec<String>,
51 secrets: Vec<ArtifactStepSecret>,
52 source: Option<String>,
53 tests: bool,
54 systems: Vec<ArtifactSystem>,
55}
56
57fn parse_cargo(path: &str) -> Result<RustCargoToml> {
58 let contents = read_to_string(path).expect("Failed to read Cargo.toml");
59
60 Ok(from_str(&contents).expect("Failed to parse Cargo.toml"))
61}
62
63impl<'a> Rust<'a> {
64 pub fn new(name: &'a str, systems: Vec<ArtifactSystem>) -> Self {
65 Self {
66 artifacts: vec![],
67 bins: vec![],
68 build: true,
69 check: false,
70 environments: vec![],
71 excludes: vec![],
72 format: false,
73 includes: vec![],
74 lint: false,
75 name,
76 packages: vec![],
77 secrets: vec![],
78 source: None,
79 tests: false,
80 systems,
81 }
82 }
83
84 pub fn with_artifacts(mut self, artifacts: Vec<String>) -> Self {
85 self.artifacts = artifacts;
86 self
87 }
88
89 pub fn with_bins(mut self, bins: Vec<&str>) -> Self {
90 self.bins = bins.iter().map(|s| s.to_string()).collect();
91 self
92 }
93
94 pub fn with_check(mut self, check: bool) -> Self {
95 self.check = check;
96 self
97 }
98
99 pub fn with_environments(mut self, environments: Vec<&'a str>) -> Self {
100 self.environments = environments;
101 self
102 }
103
104 pub fn with_excludes(mut self, excludes: Vec<&'a str>) -> Self {
105 self.excludes = excludes;
106 self
107 }
108
109 pub fn with_format(mut self, format: bool) -> Self {
110 self.format = format;
111 self
112 }
113
114 pub fn with_includes(mut self, includes: Vec<&'a str>) -> Self {
115 self.includes = includes;
116 self
117 }
118
119 pub fn with_lint(mut self, lint: bool) -> Self {
120 self.lint = lint;
121 self
122 }
123
124 pub fn with_packages(mut self, packages: Vec<&'a str>) -> Self {
125 self.packages = packages.iter().map(|s| s.to_string()).collect();
126 self
127 }
128
129 pub fn with_secrets(mut self, secrets: Vec<(&str, &str)>) -> Self {
130 for (name, value) in secrets {
131 if !self.secrets.iter().any(|s| s.name == name) {
132 self.secrets.push(ArtifactStepSecret {
133 name: name.to_string(),
134 value: value.to_string(),
135 });
136 }
137 }
138
139 self
140 }
141
142 pub fn with_source(mut self, source: String) -> Self {
143 self.source = Some(source);
144 self
145 }
146
147 pub fn with_tests(mut self, tests: bool) -> Self {
148 self.tests = tests;
149 self
150 }
151
152 pub async fn build(mut self, context: &mut ConfigContext) -> Result<String> {
153 self.secrets.sort_by(|a, b| a.name.cmp(&b.name));
155
156 let protoc = Protoc::new().build(context).await?;
157
158 let context_path = context.get_artifact_context_path();
161
162 let source_path = match self.source {
163 Some(ref source) => source,
164 None => ".",
165 };
166
167 let context_path_source = context_path.join(source_path);
168
169 if !context_path_source.exists() {
170 bail!("`source.{}.path` not found: {}", self.name, source_path);
171 }
172
173 let source_cargo_path = context_path_source.join("Cargo.toml");
176
177 if !source_cargo_path.exists() {
178 bail!("Cargo.toml not found: {:?}", source_cargo_path);
179 }
180
181 let source_cargo = parse_cargo(source_cargo_path.to_str().unwrap())?;
182
183 let mut packages = vec![];
186 let mut packages_bin_names = vec![];
187 let mut packages_manifests = vec![];
188 let mut packages_targets = vec![];
189
190 if let Some(workspace) = source_cargo.workspace {
191 if let Some(members) = workspace.members {
192 for member in members {
193 let package_path = context_path_source.join(member.clone());
194 let package_cargo_path = package_path.join("Cargo.toml");
195
196 if !package_cargo_path.exists() {
197 bail!("Cargo.toml not found: {:?}", package_cargo_path);
198 }
199
200 let package_cargo = parse_cargo(package_cargo_path.to_str().unwrap())?;
201
202 if !self.packages.is_empty() {
203 if let Some(ref package) = package_cargo.package {
204 if !self.packages.contains(&package.name) {
205 continue;
206 }
207 }
208 }
209
210 let mut package_target_paths = vec![];
211
212 if let Some(bins) = package_cargo.bin {
213 for bin in bins {
214 package_target_paths.push(package_path.join(bin.path));
215
216 if self.bins.is_empty() || self.bins.contains(&bin.name) {
217 let manifest_path = package_cargo_path.display().to_string();
218
219 if !packages_manifests.contains(&manifest_path) {
220 packages_manifests.push(manifest_path);
221 }
222
223 packages_bin_names.push(bin.name);
224 }
225 }
226 }
227
228 if package_target_paths.is_empty() {
229 package_target_paths.push(package_path.join("src/lib.rs"));
230 }
231
232 for member_target_path in package_target_paths.iter() {
233 let member_target_path_relative = member_target_path
234 .strip_prefix(&context_path_source)
235 .unwrap_or(member_target_path)
236 .to_path_buf();
237
238 packages_targets.push(member_target_path_relative);
239 }
240
241 packages.push(member);
242 }
243 }
244 }
245
246 let rust_toolchain = RustToolchain::new().build(context).await?;
251 let rust_toolchain_target = rust_toolchain::target(context.get_system())?;
252 let rust_toolchain_version = rust_toolchain::version();
253 let rust_toolchain_name = format!("{rust_toolchain_version}-{rust_toolchain_target}");
254
255 let mut step_artifacts = vec![rust_toolchain.clone()];
258
259 let mut step_environments = vec![
260 "HOME=$VORPAL_WORKSPACE/home".to_string(),
261 format!(
262 "PATH={}",
263 format!(
264 "{}/toolchains/{}/bin",
265 get_env_key(&rust_toolchain),
266 rust_toolchain_name
267 )
268 ),
269 format!("RUSTUP_HOME={}", get_env_key(&rust_toolchain)),
270 format!("RUSTUP_TOOLCHAIN={}", rust_toolchain_name),
271 ];
272
273 for environment in self.environments {
274 step_environments.push(environment.to_string());
275 }
276
277 let mut vendor_cargo_paths = vec!["Cargo.toml".to_string(), "Cargo.lock".to_string()];
280
281 for package in packages.iter() {
282 vendor_cargo_paths.push(format!("{package}/Cargo.toml"));
283 }
284
285 let mut vendor_step_script = formatdoc! {r#"
286 mkdir -p $HOME
287
288 pushd ./source/{name}-vendor"#,
289 name = self.name,
290 };
291
292 if !packages.is_empty() {
293 vendor_step_script = formatdoc! {r#"
294 {vendor_step_script}
295
296 cat > Cargo.toml << "EOF"
297 [workspace]
298 members = [{packages}]
299 resolver = "2"
300 EOF
301
302 target_paths=({target_paths})
303
304 for target_path in ${{target_paths[@]}}; do
305 mkdir -p $(dirname ${{target_path}})
306 touch ${{target_path}}
307 done"#,
308 packages = packages.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(","),
309 target_paths = packages_targets.iter().map(|s| format!("\"{}\"", s.display())).collect::<Vec<_>>().join(" "),
310 };
311 } else {
312 vendor_step_script = formatdoc! {r#"
313 {vendor_step_script}
314
315 mkdir -p src
316 touch src/main.rs"#,
317 };
318 }
319
320 vendor_step_script = formatdoc! {r#"
321 {vendor_step_script}
322
323 mkdir -p $VORPAL_OUTPUT/vendor
324
325 cargo_vendor=$(cargo vendor --versioned-dirs $VORPAL_OUTPUT/vendor)
326
327 echo "$cargo_vendor" > $VORPAL_OUTPUT/config.toml"#,
328 };
329
330 let vendor_steps = vec![
331 step::shell(
332 context,
333 step_artifacts.clone(),
334 step_environments.clone(),
335 vendor_step_script,
336 self.secrets.clone(),
337 )
338 .await?,
339 ];
340
341 let vendor_name = format!("{}-vendor", self.name);
342
343 let vendor_source = ArtifactSource::new(vendor_name.as_str(), source_path)
344 .with_includes(vendor_cargo_paths.clone())
345 .build();
346
347 let vendor = Artifact::new(vendor_name.as_str(), vendor_steps, self.systems.clone())
348 .with_sources(vec![vendor_source])
349 .build(context)
350 .await?;
351
352 step_artifacts.push(vendor.clone());
353 step_artifacts.push(protoc);
354
355 let mut source_includes = vec![];
358 let mut source_excludes = vec!["target".to_string()];
359
360 for exclude in self.excludes {
361 source_excludes.push(exclude.to_string());
362 }
363
364 for include in self.includes {
365 source_includes.push(include.to_string());
366 }
367
368 let source = ArtifactSource::new(self.name, source_path)
369 .with_includes(source_includes)
370 .with_excludes(source_excludes)
371 .build();
372
373 let mut step_script = formatdoc! {r#"
376 mkdir -p $HOME
377
378 pushd ./source/{name}
379
380 mkdir -p .cargo
381 mkdir -p $VORPAL_OUTPUT/bin
382
383 ln -s {vendor}/config.toml .cargo/config.toml"#,
384 name = self.name,
385 vendor = get_env_key(&vendor),
386 };
387
388 if !self.packages.is_empty() {
389 step_script = formatdoc! {r#"
390 {step_script}
391
392 cat > Cargo.toml << "EOF"
393 [workspace]
394 members = [{packages}]
395 resolver = "2"
396 EOF"#,
397 packages = packages.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(","),
398 };
399 }
400
401 if packages_bin_names.is_empty() {
402 packages_bin_names.push(self.name.to_string());
403 }
404
405 if packages_manifests.is_empty() {
406 packages_manifests.push(source_cargo_path.display().to_string());
407 }
408
409 step_script = formatdoc! {r#"
410 {step_script}
411
412 bin_names=({bin_names})
413 manifest_paths=({manifest_paths})
414
415 if [ "{enable_format}" = "true" ]; then
416 echo "Running formatter..."
417 cargo --offline fmt --all --check
418 fi
419
420 for manifest_path in ${{manifest_paths[@]}}; do
421 if [ "{enable_lint}" = "true" ]; then
422 echo "Running linter..."
423 cargo --offline clippy --manifest-path ${{manifest_path}} -- --deny warnings
424 fi
425 done
426
427 for bin_name in ${{bin_names[@]}}; do
428 if [ "{enable_check}" = "true" ]; then
429 echo "Running check..."
430 cargo --offline check --bin ${{bin_name}} --release
431 fi
432
433 if [ "{enable_build}" = "true" ]; then
434 echo "Running build..."
435 cargo --offline build --bin ${{bin_name}} --release
436 fi
437
438 if [ "{enable_tests}" = "true" ]; then
439 echo "Running tests..."
440 cargo --offline test --bin ${{bin_name}} --release
441 fi
442
443 cp -p ./target/release/${{bin_name}} $VORPAL_OUTPUT/bin/
444 done"#,
445 bin_names = packages_bin_names.join(" "),
446 enable_build = if self.build { "true" } else { "false" },
447 enable_check = if self.check { "true" } else { "false" },
448 enable_format = if self.format { "true" } else { "false" },
449 enable_lint = if self.lint { "true" } else { "false" },
450 enable_tests = if self.tests { "true" } else { "false" },
451 manifest_paths = packages_manifests.join(" "),
452 };
453
454 let steps = vec![
455 step::shell(
456 context,
457 [step_artifacts.clone(), self.artifacts.clone()].concat(),
458 step_environments,
459 step_script,
460 self.secrets,
461 )
462 .await?,
463 ];
464
465 Artifact::new(self.name, steps, self.systems)
468 .with_sources(vec![source])
469 .build(context)
470 .await
471 }
472}
473
474pub struct RustDevelopmentEnvironment<'a> {
479 artifacts: Vec<String>,
480 environments: Vec<String>,
481 include_protoc: bool,
482 name: &'a str,
483 secrets: Vec<(&'a str, &'a str)>,
484 systems: Vec<ArtifactSystem>,
485}
486
487impl<'a> RustDevelopmentEnvironment<'a> {
488 pub fn new(name: &'a str, systems: Vec<ArtifactSystem>) -> Self {
489 Self {
490 artifacts: vec![],
491 environments: vec![],
492 include_protoc: true,
493 name,
494 secrets: vec![],
495 systems,
496 }
497 }
498
499 pub fn with_artifacts(mut self, artifacts: Vec<String>) -> Self {
500 self.artifacts.extend(artifacts);
501 self
502 }
503
504 pub fn with_environments(mut self, environments: Vec<String>) -> Self {
505 self.environments.extend(environments);
506 self
507 }
508
509 pub fn without_protoc(mut self) -> Self {
510 self.include_protoc = false;
511 self
512 }
513
514 pub fn with_secrets(mut self, secrets: Vec<(&'a str, &'a str)>) -> Self {
515 for secret in secrets {
516 if !self.secrets.iter().any(|(name, _)| *name == secret.0) {
517 self.secrets.push(secret);
518 }
519 }
520 self
521 }
522
523 pub async fn build(self, context: &mut ConfigContext) -> Result<String> {
524 let rust_toolchain_digest = RustToolchain::new().build(context).await?;
525
526 let mut artifacts = vec![];
527
528 if self.include_protoc {
529 let protoc = Protoc::new().build(context).await?;
530 artifacts.push(protoc);
531 }
532
533 artifacts.push(rust_toolchain_digest.clone());
534 artifacts.extend(self.artifacts);
535
536 let toolchain_target = rust_toolchain::target(context.get_system())?;
537 let toolchain_version = rust_toolchain::version();
538 let toolchain_name = format!("{toolchain_version}-{toolchain_target}");
539 let toolchain_bin = format!(
540 "{}/toolchains/{toolchain_name}/bin",
541 get_env_key(&rust_toolchain_digest)
542 );
543
544 let mut environments = vec![
545 format!("PATH={toolchain_bin}"),
546 format!("RUSTUP_HOME={}", get_env_key(&rust_toolchain_digest)),
547 format!("RUSTUP_TOOLCHAIN={toolchain_name}"),
548 ];
549
550 environments.extend(self.environments);
551
552 let mut devenv = DevelopmentEnvironment::new(self.name, self.systems)
553 .with_artifacts(artifacts)
554 .with_environments(environments);
555
556 if !self.secrets.is_empty() {
557 devenv = devenv.with_secrets(self.secrets);
558 }
559
560 devenv.build(context).await
561 }
562}