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 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 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 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 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 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 self.symlinks.sort_by(|a, b| a.0.cmp(&b.0));
589
590 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 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}