Skip to main content

vorpal_sdk/
artifact.rs

1use crate::{api, context};
2use anyhow::{bail, Result};
3use indoc::formatdoc;
4
5pub mod bun;
6pub mod cargo;
7pub mod clippy;
8pub mod crane;
9pub mod gh;
10pub mod git;
11pub mod go;
12pub mod goimports;
13pub mod gopls;
14pub mod grpcurl;
15pub mod language;
16pub mod linux_debian;
17pub mod linux_vorpal;
18pub mod linux_vorpal_slim;
19pub mod nodejs;
20pub mod oci_image;
21pub mod pnpm;
22pub mod protoc;
23pub mod protoc_gen_go;
24pub mod protoc_gen_go_grpc;
25pub mod rsync;
26pub mod rust_analyzer;
27pub mod rust_src;
28pub mod rust_std;
29pub mod rust_toolchain;
30pub mod rustc;
31pub mod rustfmt;
32pub mod staticcheck;
33pub mod step;
34pub mod system;
35
36pub struct Argument<'a> {
37    pub name: &'a str,
38    pub require: bool,
39}
40
41pub struct ArtifactSource<'a> {
42    pub digest: Option<&'a str>,
43    pub excludes: Vec<String>,
44    pub includes: Vec<String>,
45    pub name: &'a str,
46    pub path: &'a str,
47}
48
49pub struct ArtifactStep<'a> {
50    pub arguments: Vec<String>,
51    pub artifacts: Vec<String>,
52    pub entrypoint: &'a str,
53    pub environments: Vec<String>,
54    pub secrets: Vec<api::artifact::ArtifactStepSecret>,
55    pub script: Option<String>,
56}
57
58pub struct Artifact<'a> {
59    pub aliases: Vec<String>,
60    pub name: &'a str,
61    pub sources: Vec<api::artifact::ArtifactSource>,
62    pub steps: Vec<api::artifact::ArtifactStep>,
63    pub systems: Vec<api::artifact::ArtifactSystem>,
64}
65
66pub struct Job<'a> {
67    pub artifacts: Vec<String>,
68    pub name: &'a str,
69    pub secrets: Vec<api::artifact::ArtifactStepSecret>,
70    pub script: String,
71    pub systems: Vec<api::artifact::ArtifactSystem>,
72}
73
74pub struct Process<'a> {
75    pub arguments: Vec<String>,
76    pub artifacts: Vec<String>,
77    pub entrypoint: &'a str,
78    pub name: &'a str,
79    pub secrets: Vec<api::artifact::ArtifactStepSecret>,
80    pub systems: Vec<api::artifact::ArtifactSystem>,
81}
82
83pub struct DevelopmentEnvironment<'a> {
84    pub artifacts: Vec<String>,
85    pub environments: Vec<String>,
86    pub name: &'a str,
87    pub secrets: Vec<api::artifact::ArtifactStepSecret>,
88    pub systems: Vec<api::artifact::ArtifactSystem>,
89}
90
91pub struct UserEnvironment<'a> {
92    pub artifacts: Vec<String>,
93    pub environments: Vec<String>,
94    pub name: &'a str,
95    pub symlinks: Vec<(String, String)>,
96    pub systems: Vec<api::artifact::ArtifactSystem>,
97}
98
99impl<'a> Argument<'a> {
100    pub fn new(name: &'a str) -> Self {
101        Self {
102            name,
103            require: false,
104        }
105    }
106
107    pub fn with_require(mut self) -> Self {
108        self.require = true;
109        self
110    }
111
112    pub fn build(self, context: &mut context::ConfigContext) -> Result<Option<String>> {
113        let variable = context.get_variable(self.name);
114
115        if self.require && variable.is_none() {
116            bail!("variable '{}' is required", self.name)
117        }
118
119        Ok(variable)
120    }
121}
122
123impl<'a> ArtifactSource<'a> {
124    pub fn new(name: &'a str, path: &'a str) -> Self {
125        Self {
126            digest: None,
127            excludes: vec![],
128            includes: vec![],
129            name,
130            path,
131        }
132    }
133
134    pub fn with_digest(mut self, digest: &'a str) -> Self {
135        self.digest = Some(digest);
136        self
137    }
138
139    pub fn with_excludes(mut self, excludes: Vec<String>) -> Self {
140        self.excludes = excludes;
141        self
142    }
143
144    pub fn with_includes(mut self, includes: Vec<String>) -> Self {
145        self.includes = includes;
146        self
147    }
148
149    pub fn build(self) -> api::artifact::ArtifactSource {
150        api::artifact::ArtifactSource {
151            digest: self.digest.map(|v| v.to_string()),
152            includes: self.includes,
153            excludes: self.excludes,
154            name: self.name.to_string(),
155            path: self.path.to_string(),
156        }
157    }
158}
159
160impl<'a> ArtifactStep<'a> {
161    pub fn new(entrypoint: &'a str) -> Self {
162        Self {
163            arguments: vec![],
164            artifacts: vec![],
165            entrypoint,
166            environments: vec![],
167            secrets: vec![],
168            script: None,
169        }
170    }
171
172    pub fn with_arguments(mut self, arguments: Vec<&str>) -> Self {
173        self.arguments = arguments.iter().map(|v| v.to_string()).collect();
174        self
175    }
176
177    pub fn with_artifacts(mut self, artifacts: Vec<String>) -> Self {
178        self.artifacts = artifacts;
179        self
180    }
181
182    pub fn with_environments(mut self, environments: Vec<String>) -> Self {
183        self.environments = environments;
184        self
185    }
186
187    pub fn with_secrets(mut self, secrets: Vec<api::artifact::ArtifactStepSecret>) -> Self {
188        for secret in secrets {
189            if !self.secrets.iter().any(|s| s.name == secret.name) {
190                self.secrets.push(secret);
191            }
192        }
193        self
194    }
195
196    pub fn with_script(mut self, script: String) -> Self {
197        self.script = Some(script);
198        self
199    }
200
201    pub fn build(self) -> api::artifact::ArtifactStep {
202        api::artifact::ArtifactStep {
203            arguments: self.arguments,
204            artifacts: self.artifacts,
205            entrypoint: Some(self.entrypoint.to_string()),
206            environments: self.environments,
207            secrets: self.secrets,
208            script: self.script,
209        }
210    }
211}
212
213impl<'a> Artifact<'a> {
214    pub fn new(
215        name: &'a str,
216        steps: Vec<api::artifact::ArtifactStep>,
217        systems: Vec<api::artifact::ArtifactSystem>,
218    ) -> Self {
219        Self {
220            aliases: vec![],
221            name,
222            sources: vec![],
223            steps,
224            systems,
225        }
226    }
227
228    pub fn with_aliases(mut self, aliases: Vec<String>) -> Self {
229        for alias in aliases {
230            if !self.aliases.contains(&alias) {
231                self.aliases.push(alias);
232            }
233        }
234        self
235    }
236
237    pub fn with_sources(mut self, sources: Vec<api::artifact::ArtifactSource>) -> Self {
238        for source in sources {
239            if !self.sources.iter().any(|s| s.name == source.name) {
240                self.sources.push(source);
241            }
242        }
243
244        self
245    }
246
247    pub async fn build(self, context: &mut context::ConfigContext) -> Result<String> {
248        let artifact = api::artifact::Artifact {
249            aliases: self.aliases,
250            name: self.name.to_string(),
251            sources: self.sources,
252            steps: self.steps,
253            systems: self.systems.into_iter().map(|v| v.into()).collect(),
254            target: context.get_system().into(),
255        };
256
257        context.add_artifact(&artifact).await
258    }
259}
260
261impl<'a> Job<'a> {
262    pub fn new(name: &'a str, script: String, systems: Vec<api::artifact::ArtifactSystem>) -> Self {
263        Self {
264            artifacts: vec![],
265            name,
266            secrets: vec![],
267            script,
268            systems,
269        }
270    }
271
272    pub fn with_artifacts(mut self, artifacts: Vec<String>) -> Self {
273        self.artifacts = artifacts;
274        self
275    }
276
277    pub fn with_secrets(mut self, secrets: Vec<(&str, &str)>) -> Self {
278        for (name, value) in secrets {
279            if !self.secrets.iter().any(|s| s.name == name) {
280                self.secrets.push(api::artifact::ArtifactStepSecret {
281                    name: name.to_string(),
282                    value: value.to_string(),
283                });
284            }
285        }
286
287        self
288    }
289
290    pub async fn build(mut self, context: &mut context::ConfigContext) -> Result<String> {
291        // Sort for deterministic output
292        self.secrets.sort_by(|a, b| a.name.cmp(&b.name));
293
294        let step = step::shell(context, self.artifacts, vec![], self.script, self.secrets).await?;
295
296        Artifact::new(self.name, vec![step], self.systems)
297            .build(context)
298            .await
299    }
300}
301
302impl<'a> DevelopmentEnvironment<'a> {
303    pub fn new(name: &'a str, systems: Vec<api::artifact::ArtifactSystem>) -> Self {
304        Self {
305            artifacts: vec![],
306            environments: vec![],
307            name,
308            secrets: vec![],
309            systems,
310        }
311    }
312
313    pub fn with_artifacts(mut self, artifacts: Vec<String>) -> Self {
314        self.artifacts = artifacts;
315        self
316    }
317
318    pub fn with_environments(mut self, environments: Vec<String>) -> Self {
319        self.environments = environments;
320        self
321    }
322
323    pub fn with_secrets(mut self, secrets: Vec<(&str, &str)>) -> Self {
324        for (name, value) in secrets.into_iter() {
325            if !self.secrets.iter().any(|s| s.name == name) {
326                self.secrets.push(api::artifact::ArtifactStepSecret {
327                    name: name.to_string(),
328                    value: value.to_string(),
329                });
330            }
331        }
332
333        self
334    }
335
336    pub async fn build(mut self, context: &mut context::ConfigContext) -> Result<String> {
337        // Sort for deterministic output
338        self.secrets.sort_by(|a, b| a.name.cmp(&b.name));
339
340        let mut envs_backup = vec![
341            "export VORPAL_SHELL_BACKUP_PATH=\"$PATH\"".to_string(),
342            "export VORPAL_SHELL_BACKUP_PS1=\"$PS1\"".to_string(),
343            "export VORPAL_SHELL_BACKUP_VORPAL_SHELL=\"$VORPAL_SHELL\"".to_string(),
344        ];
345
346        let mut envs_export = vec![
347            format!("export PS1=\"({}) $PS1\"", self.name),
348            "export VORPAL_SHELL=\"1\"".to_string(),
349        ];
350
351        let mut envs_restore = vec![
352            "export PATH=\"$VORPAL_SHELL_BACKUP_PATH\"".to_string(),
353            "export PS1=\"$VORPAL_SHELL_BACKUP_PS1\"".to_string(),
354            "export VORPAL_SHELL=\"$VORPAL_SHELL_BACKUP_VORPAL_SHELL\"".to_string(),
355        ];
356
357        let mut envs_unset = vec![
358            "unset VORPAL_SHELL_BACKUP_PATH".to_string(),
359            "unset VORPAL_SHELL_BACKUP_PS1".to_string(),
360            "unset VORPAL_SHELL_BACKUP_VORPAL_SHELL".to_string(),
361        ];
362
363        for env in self.environments.clone().into_iter() {
364            let key = env.split("=").next().unwrap();
365
366            if key == "PATH" {
367                continue;
368            }
369
370            envs_backup.push(format!("export VORPAL_SHELL_BACKUP_{key}=\"${key}\""));
371            envs_export.push(format!("export {env}"));
372            envs_restore.push(format!("export {key}=\"$VORPAL_SHELL_BACKUP_{key}\""));
373            envs_unset.push(format!("unset VORPAL_SHELL_BACKUP_{key}"));
374        }
375
376        // Setup path
377
378        let step_path_artifacts = self
379            .artifacts
380            .iter()
381            .map(|artifact| format!("{}/bin", get_env_key(artifact)))
382            .collect::<Vec<String>>()
383            .join(":");
384
385        let mut step_path = step_path_artifacts;
386
387        if let Some(path) = self.environments.iter().find(|x| x.starts_with("PATH=")) {
388            if let Some(path_value) = path.split('=').nth(1) {
389                step_path = format!("{path_value}:{step_path}");
390            }
391        }
392
393        envs_export.push(format!("export PATH={step_path}:$PATH"));
394
395        // Setup script
396
397        let step_script = formatdoc! {"
398            mkdir -p $VORPAL_WORKSPACE/bin
399
400            cat > bin/activate << \"EOF\"
401            #!/bin/bash
402
403            {backups}
404            {exports}
405
406            deactivate(){{
407            {restores}
408            {unsets}
409            }}
410
411            exec \"$@\"
412            EOF
413
414            chmod +x $VORPAL_WORKSPACE/bin/activate
415
416            mkdir -p $VORPAL_OUTPUT/bin
417
418            cp -pr bin \"$VORPAL_OUTPUT\"",
419            backups = envs_backup.join("\n"),
420            exports = envs_export.join("\n"),
421            restores = envs_restore.join("\n"),
422            unsets = envs_unset.join("\n"),
423        };
424
425        let steps =
426            vec![step::shell(context, self.artifacts, vec![], step_script, self.secrets).await?];
427
428        Artifact::new(self.name, steps, self.systems)
429            .build(context)
430            .await
431    }
432}
433
434impl<'a> Process<'a> {
435    pub fn new(
436        name: &'a str,
437        entrypoint: &'a str,
438        systems: Vec<api::artifact::ArtifactSystem>,
439    ) -> Self {
440        Self {
441            arguments: vec![],
442            artifacts: vec![],
443            entrypoint,
444            name,
445            secrets: vec![],
446            systems,
447        }
448    }
449
450    pub fn with_arguments(mut self, arguments: Vec<&str>) -> Self {
451        self.arguments = arguments.iter().map(|v| v.to_string()).collect();
452        self
453    }
454
455    pub fn with_artifacts(mut self, artifacts: Vec<String>) -> Self {
456        for artifact in artifacts {
457            if !self.artifacts.contains(&artifact) {
458                self.artifacts.push(artifact);
459            }
460        }
461        self
462    }
463
464    pub fn with_secrets(mut self, secrets: Vec<(&str, &str)>) -> Self {
465        for (name, value) in secrets {
466            if !self.secrets.iter().any(|s| s.name == name) {
467                self.secrets.push(api::artifact::ArtifactStepSecret {
468                    name: name.to_string(),
469                    value: value.to_string(),
470                });
471            }
472        }
473
474        self
475    }
476
477    pub async fn build(mut self, context: &mut context::ConfigContext) -> Result<String> {
478        // Sort for deterministic output
479        self.secrets.sort_by(|a, b| a.name.cmp(&b.name));
480
481        let script = formatdoc! {r#"
482            mkdir -p $VORPAL_OUTPUT/bin
483
484            cat > $VORPAL_OUTPUT/bin/{name}-logs << "EOF"
485            #!/bin/bash
486            set -euo pipefail
487
488            if [ -f $VORPAL_OUTPUT/logs.txt ]; then
489                tail -f $VORPAL_OUTPUT/logs.txt
490            else
491                echo "No logs found"
492            fi
493            EOF
494
495            chmod +x $VORPAL_OUTPUT/bin/{name}-logs
496
497            cat > $VORPAL_OUTPUT/bin/{name}-stop << "EOF"
498            #!/bin/bash
499            set -euo pipefail
500
501            if [ -f $VORPAL_OUTPUT/pid ]; then
502                kill $(cat $VORPAL_OUTPUT/pid)
503                rm -rf $VORPAL_OUTPUT/pid
504            fi
505            EOF
506
507            chmod +x $VORPAL_OUTPUT/bin/{name}-stop
508
509            cat > $VORPAL_OUTPUT/bin/{name}-start << "EOF"
510            #!/bin/bash
511            set -euo pipefail
512
513            export PATH={artifacts}:$PATH
514
515            $VORPAL_OUTPUT/bin/{name}-stop
516
517            echo "Process: {entrypoint} {arguments}"
518
519            nohup {entrypoint} {arguments} > $VORPAL_OUTPUT/logs.txt 2>&1 &
520
521            PROCESS_PID=$!
522
523            echo "Process ID: $PROCESS_PID"
524
525            echo $PROCESS_PID > $VORPAL_OUTPUT/pid
526
527            echo "Process commands:"
528            echo "- {name}-logs (tail logs)"
529            echo "- {name}-stop (stop process)"
530            echo "- {name}-start (start process)"
531            EOF
532
533            chmod +x $VORPAL_OUTPUT/bin/{name}-start"#,
534            arguments = self
535                .arguments
536                .iter()
537                .map(|v| v.to_string())
538                .collect::<Vec<String>>()
539                .join(" "),
540            artifacts = self
541                .artifacts
542                .iter()
543                .map(|v| format!("$VORPAL_ARTIFACT_{v}/bin"))
544                .collect::<Vec<String>>()
545                .join(":"),
546            entrypoint = self.entrypoint,
547            name = self.name,
548        };
549
550        let step = step::shell(context, self.artifacts, vec![], script, self.secrets).await?;
551
552        Artifact::new(self.name, vec![step], self.systems)
553            .build(context)
554            .await
555    }
556}
557
558impl<'a> UserEnvironment<'a> {
559    pub fn new(name: &'a str, systems: Vec<api::artifact::ArtifactSystem>) -> Self {
560        Self {
561            artifacts: vec![],
562            environments: vec![],
563            name,
564            symlinks: vec![],
565            systems,
566        }
567    }
568
569    pub fn with_artifacts(mut self, artifacts: Vec<String>) -> Self {
570        self.artifacts = artifacts;
571        self
572    }
573
574    pub fn with_environments(mut self, environments: Vec<String>) -> Self {
575        self.environments = environments;
576        self
577    }
578
579    pub fn with_symlinks(mut self, symlinks: Vec<(&str, &str)>) -> Self {
580        for (source, target) in symlinks.into_iter() {
581            self.symlinks.push((source.to_string(), target.to_string()));
582        }
583        self
584    }
585
586    pub async fn build(mut self, context: &mut context::ConfigContext) -> Result<String> {
587        // Sort for deterministic output
588        self.symlinks.sort_by(|a, b| a.0.cmp(&b.0));
589
590        // Setup path
591
592        let step_path_artifacts = self
593            .artifacts
594            .iter()
595            .map(|artifact| format!("{}/bin", get_env_key(artifact)))
596            .collect::<Vec<String>>()
597            .join(":");
598
599        let mut step_path = step_path_artifacts;
600
601        if let Some(path) = self.environments.iter().find(|x| x.starts_with("PATH=")) {
602            if let Some(path_value) = path.split('=').nth(1) {
603                step_path = format!("{path_value}:{step_path}");
604            }
605        }
606
607        // Setup script
608
609        let step_script = formatdoc! {r#"
610            mkdir -p $VORPAL_OUTPUT/bin
611
612            cat > $VORPAL_OUTPUT/bin/vorpal-activate-shell << "EOF"
613            {environments}
614            export PATH="$VORPAL_OUTPUT/bin:{step_path}:$PATH"
615            EOF
616
617            cat > $VORPAL_OUTPUT/bin/vorpal-deactivate-symlinks << "EOF"
618            #!/bin/bash
619            set -euo pipefail
620            {symlinks_deactivate}
621            EOF
622
623            cat > $VORPAL_OUTPUT/bin/vorpal-activate-symlinks << "EOF"
624            #!/bin/bash
625            set -euo pipefail
626            {symlinks_check}
627            {symlinks_activate}
628            EOF
629
630            cat > $VORPAL_OUTPUT/bin/vorpal-activate << "EOF"
631            #!/bin/bash
632            set -euo pipefail
633
634            echo "Deactivating previous symlinks..."
635
636            if [ -f $HOME/.vorpal/bin/vorpal-deactivate-symlinks ]; then
637                $HOME/.vorpal/bin/vorpal-deactivate-symlinks
638            fi
639
640            echo "Activating symlinks..."
641
642            $VORPAL_OUTPUT/bin/vorpal-activate-symlinks
643
644            echo "Vorpal userenv installed. Run 'source vorpal-activate-shell' to activate."
645
646            ln -sf $VORPAL_OUTPUT/bin/vorpal-activate-shell $HOME/.vorpal/bin/vorpal-activate-shell
647            ln -sf $VORPAL_OUTPUT/bin/vorpal-activate-symlinks $HOME/.vorpal/bin/vorpal-activate-symlinks
648            ln -sf $VORPAL_OUTPUT/bin/vorpal-deactivate-symlinks $HOME/.vorpal/bin/vorpal-deactivate-symlinks
649            EOF
650
651
652            chmod +x $VORPAL_OUTPUT/bin/vorpal-activate-shell
653            chmod +x $VORPAL_OUTPUT/bin/vorpal-deactivate-symlinks
654            chmod +x $VORPAL_OUTPUT/bin/vorpal-activate-symlinks
655            chmod +x $VORPAL_OUTPUT/bin/vorpal-activate"#,
656            environments = self.environments
657                .iter()
658                .filter(|e| !e.starts_with("PATH="))
659                .map(|e| format!("export {e}"))
660                .collect::<Vec<String>>()
661                .join("\n"),
662            symlinks_deactivate = self.symlinks
663                .iter()
664                .map(|(_, target)| format!("rm -f {target}"))
665                .collect::<Vec<String>>()
666                .join("\n"),
667            symlinks_check = self.symlinks
668                .iter()
669                .map(|(_, target)| format!("if [ -f {target} ]; then echo \"ERROR: Symlink target exists -> {target}\" && exit 1; fi"))
670                .collect::<Vec<String>>()
671                .join("\n"),
672            symlinks_activate = self.symlinks
673                .iter()
674                .map(|(source, target)| format!("ln -s {source} {target}"))
675                .collect::<Vec<String>>()
676                .join("\n"),
677        };
678
679        let steps = vec![step::shell(context, self.artifacts, vec![], step_script, vec![]).await?];
680
681        Artifact::new(self.name, steps, self.systems)
682            .build(context)
683            .await
684    }
685}
686
687pub fn get_default_address() -> String {
688    if let Ok(path) = std::env::var("VORPAL_SOCKET_PATH") {
689        if !path.is_empty() {
690            return format!("unix://{}", path);
691        }
692    }
693    "unix:///var/lib/vorpal/vorpal.sock".to_string()
694}
695
696pub fn get_env_key(digest: &String) -> String {
697    format!("$VORPAL_ARTIFACT_{digest}")
698}