Skip to main content

vorpal_sdk/artifact/language/
rust.rs

1use 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    // version: String,
32}
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        // Sort for deterministic output
154        self.secrets.sort_by(|a, b| a.name.cmp(&b.name));
155
156        let protoc = Protoc::new().build(context).await?;
157
158        // Parse source path
159
160        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        // Parse cargo.toml
174
175        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        // Get list of bin targets
184
185        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        // 2. CREATE ARTIFACTS
247
248        // Get rust toolchain artifact
249
250        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        // Set environment variables
256
257        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        // Create vendor artifact
278
279        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        // Create source
356
357        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        // Create step
374
375        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        // Create artifact
466
467        Artifact::new(self.name, steps, self.systems)
468            .with_sources(vec![source])
469            .build(context)
470            .await
471    }
472}
473
474// ---------------------------------------------------------------------------
475// Rust Development Environment
476// ---------------------------------------------------------------------------
477
478pub 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}