├── book ├── .gitignore ├── theme │ ├── favicon.svg │ ├── ayu-highlight.css │ └── highlight.css ├── src │ ├── images │ │ ├── logo-title.png │ │ ├── raptor-make.png │ │ ├── deblive-grub.png │ │ ├── raptor-circle-icon.png │ │ └── raptor-layers.pikchr │ ├── mount-types │ │ ├── simple.md │ │ ├── file.md │ │ ├── overlay.md │ │ └── layers.md │ ├── inst │ │ ├── entrypoint.md │ │ ├── mkdir.md │ │ ├── render.md │ │ ├── run.md │ │ ├── env.md │ │ ├── workdir.md │ │ ├── write.md │ │ ├── copy.md │ │ ├── include.md │ │ ├── from.md │ │ ├── mount.md │ │ └── cmd.md │ ├── syntax.md │ ├── module-name │ │ ├── relative.md │ │ ├── absolute.md │ │ └── package.md │ ├── walkthrough │ │ └── debian │ │ │ ├── index.md │ │ │ ├── build.md │ │ │ └── make.md │ ├── module-name.md │ ├── instancing.md │ ├── SUMMARY.md │ ├── make.md │ ├── string-escape.md │ ├── expressions.md │ ├── builders │ │ ├── part-image.md │ │ ├── docker-image.md │ │ ├── disk-image.md │ │ ├── deblive.md │ │ ├── environment.md │ │ ├── live-disk-image.md │ │ └── index.md │ ├── install.md │ ├── file-options.md │ ├── intro.md │ ├── mount-types.md │ └── grammar.md ├── example │ ├── file-lister.rapt │ ├── layers-lister.rapt │ ├── overlay-lister.rapt │ ├── file-lister-output.rapt │ ├── ping.rapt │ ├── ssh.rapt │ └── base.rapt ├── book.toml └── raptorfile.js ├── tests ├── cases │ ├── error │ │ ├── dummy.out │ │ ├── error_missing_from.rapt │ │ ├── dummy.rapt │ │ ├── error_missing_include.rinc │ │ ├── error_invalid_jinja.rapt │ │ ├── error_failing_instruction.rapt │ │ ├── error_missing_include.rapt │ │ ├── error_undefined_include_var.rapt │ │ ├── error_recursive_include.rapt │ │ ├── error_recursive_include.rinc │ │ ├── error_missing_include_level_2.rapt │ │ ├── error_undefined_var.rapt │ │ ├── error_invalid_instruction.rapt │ │ ├── error_undefined_include_var.out │ │ ├── error_invalid_instruction.out │ │ ├── error_failing_instruction.out │ │ ├── error_missing_from.out │ │ ├── error_missing_include.out │ │ ├── error_invalid_jinja.out │ │ ├── error_undefined_var.out │ │ └── error_missing_include_level_2.out │ ├── inst │ │ ├── run01.rapt │ │ ├── cmd01.rapt │ │ ├── env01.rapt │ │ ├── copy01.rapt │ │ ├── from01.rapt │ │ ├── include │ │ │ ├── run01.rinc │ │ │ ├── include01.rinc │ │ │ ├── template01.tmpl │ │ │ └── template02.tmpl │ │ ├── include01.rinc │ │ ├── mkdir01.rapt │ │ ├── run02.rapt │ │ ├── workdir01.rapt │ │ ├── copy02.rapt │ │ ├── expr03.rapt │ │ ├── include01.rapt │ │ ├── include02.rapt │ │ ├── mkdir02.rapt │ │ ├── write01.rinc │ │ ├── cmd02.rapt │ │ ├── entrypoint01.rapt │ │ ├── env02.rapt │ │ ├── expr01.rapt │ │ ├── include03.rapt │ │ ├── mount01.rapt │ │ ├── from02.rapt │ │ ├── include04.rapt │ │ ├── run03.rapt │ │ ├── expr02.rapt │ │ ├── expr04.rapt │ │ ├── render01.rapt │ │ ├── render03.rapt │ │ ├── entrypoint02.rapt │ │ ├── write02.rapt │ │ ├── render03.rinc │ │ ├── mkdir03.rapt │ │ ├── render02.rapt │ │ └── expr05.rapt │ └── func │ │ ├── data1.yaml │ │ ├── write_dict.rapt │ │ ├── log.rapt │ │ ├── write_arg.rapt │ │ └── write_yaml.rapt ├── falcon.rs └── diagnostic.rs ├── src ├── runner │ └── mod.rs ├── make │ └── mod.rs ├── dsl │ ├── mod.rs │ ├── item.rs │ └── program.rs ├── build │ ├── mod.rs │ ├── present.rs │ └── stats.rs ├── sandbox │ ├── mod.rs │ ├── ext.rs │ └── file.rs ├── program │ ├── mod.rs │ ├── printer.rs │ ├── resolve.rs │ ├── error.rs │ └── executor.rs ├── template │ ├── load_yaml.rs │ ├── header.rs │ ├── log.rs │ ├── file.rs │ ├── escape.rs │ ├── mod.rs │ └── args.rs ├── util │ ├── kwargs.rs │ ├── tty.rs │ ├── clapcolor.rs │ ├── flag.rs │ ├── mod.rs │ └── capture_proc_fd.rs └── tui │ ├── jobstate.rs │ ├── statusbar.rs │ ├── logo.rs │ └── joblist.rs ├── crates ├── falcon │ ├── .cargo │ │ └── config.toml │ ├── src │ │ ├── lib.rs │ │ ├── client │ │ │ ├── mod.rs │ │ │ ├── frameio.rs │ │ │ └── protocol.rs │ │ ├── error.rs │ │ ├── bin │ │ │ └── falcon-test.rs │ │ └── umask_proc.rs │ └── Cargo.toml ├── dregistry │ ├── src │ │ ├── lib.rs │ │ ├── reference.pest │ │ ├── error.rs │ │ ├── digest.rs │ │ └── reference.rs │ ├── examples │ │ └── parse-reference.rs │ └── Cargo.toml └── raptor-parser │ ├── src │ ├── util │ │ ├── mod.rs │ │ ├── location.rs │ │ └── safe_parent.rs │ ├── lib.rs │ ├── ast │ │ ├── workdir.rs │ │ ├── cmd.rs │ │ ├── run.rs │ │ ├── entrypoint.rs │ │ ├── write.rs │ │ ├── copy.rs │ │ ├── mod.rs │ │ ├── mkdir.rs │ │ ├── render.rs │ │ ├── from.rs │ │ ├── env.rs │ │ ├── origin.rs │ │ ├── chown.rs │ │ ├── mount.rs │ │ └── include.rs │ ├── error.rs │ └── print │ │ └── mod.rs │ ├── Cargo.toml │ └── examples │ ├── parse.rs │ └── show-lexing.rs ├── .gitignore ├── .github └── workflows │ └── mdbook-deploy.yml └── README.md /book/.gitignore: -------------------------------------------------------------------------------- 1 | /book/ 2 | -------------------------------------------------------------------------------- /tests/cases/error/dummy.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cases/inst/run01.rapt: -------------------------------------------------------------------------------- 1 | RUN id 2 | -------------------------------------------------------------------------------- /tests/cases/inst/cmd01.rapt: -------------------------------------------------------------------------------- 1 | CMD /bin/sh 2 | -------------------------------------------------------------------------------- /tests/cases/inst/env01.rapt: -------------------------------------------------------------------------------- 1 | ENV foo=bar 2 | -------------------------------------------------------------------------------- /book/theme/favicon.svg: -------------------------------------------------------------------------------- 1 | ../branding/favicon2.svg -------------------------------------------------------------------------------- /tests/cases/inst/copy01.rapt: -------------------------------------------------------------------------------- 1 | COPY file /foo 2 | -------------------------------------------------------------------------------- /tests/cases/inst/from01.rapt: -------------------------------------------------------------------------------- 1 | FROM baselayer 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include/run01.rinc: -------------------------------------------------------------------------------- 1 | RUN id 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include01.rinc: -------------------------------------------------------------------------------- 1 | include01.rapt -------------------------------------------------------------------------------- /tests/cases/inst/mkdir01.rapt: -------------------------------------------------------------------------------- 1 | MKDIR /foo 2 | -------------------------------------------------------------------------------- /tests/cases/inst/run02.rapt: -------------------------------------------------------------------------------- 1 | RUN "ls" "-l" 2 | -------------------------------------------------------------------------------- /tests/cases/inst/workdir01.rapt: -------------------------------------------------------------------------------- 1 | WORKDIR /foo 2 | -------------------------------------------------------------------------------- /tests/cases/inst/copy02.rapt: -------------------------------------------------------------------------------- 1 | COPY a b c d /dir/ 2 | -------------------------------------------------------------------------------- /tests/cases/inst/expr03.rapt: -------------------------------------------------------------------------------- 1 | RENDER foo bar a={} 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include01.rapt: -------------------------------------------------------------------------------- 1 | INCLUDE write01 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include02.rapt: -------------------------------------------------------------------------------- 1 | INCLUDE include01 2 | -------------------------------------------------------------------------------- /tests/cases/inst/mkdir02.rapt: -------------------------------------------------------------------------------- 1 | MKDIR -p /foo/bar 2 | -------------------------------------------------------------------------------- /tests/cases/inst/write01.rinc: -------------------------------------------------------------------------------- 1 | WRITE "bar" /foo 2 | -------------------------------------------------------------------------------- /tests/cases/inst/cmd02.rapt: -------------------------------------------------------------------------------- 1 | CMD /bin/sh -c "echo foo" 2 | -------------------------------------------------------------------------------- /tests/cases/inst/entrypoint01.rapt: -------------------------------------------------------------------------------- 1 | ENTRYPOINT /bin/sh 2 | -------------------------------------------------------------------------------- /tests/cases/inst/env02.rapt: -------------------------------------------------------------------------------- 1 | ENV foo1=bar1 foo2=bar2 2 | -------------------------------------------------------------------------------- /tests/cases/inst/expr01.rapt: -------------------------------------------------------------------------------- 1 | RENDER foo bar a=[1,2,3] 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include/include01.rinc: -------------------------------------------------------------------------------- 1 | INCLUDE run01 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include/template01.tmpl: -------------------------------------------------------------------------------- 1 | Hello world 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include03.rapt: -------------------------------------------------------------------------------- 1 | INCLUDE include.run01 2 | -------------------------------------------------------------------------------- /tests/cases/inst/mount01.rapt: -------------------------------------------------------------------------------- 1 | MOUNT --simple foo /bar 2 | -------------------------------------------------------------------------------- /src/runner/mod.rs: -------------------------------------------------------------------------------- 1 | mod runner; 2 | 3 | pub use runner::*; 4 | -------------------------------------------------------------------------------- /tests/cases/error/error_missing_from.rapt: -------------------------------------------------------------------------------- 1 | FROM missingbase 2 | -------------------------------------------------------------------------------- /tests/cases/inst/from02.rapt: -------------------------------------------------------------------------------- 1 | FROM docker://debian:stable 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include/template02.tmpl: -------------------------------------------------------------------------------- 1 | Hello {{what}} 2 | -------------------------------------------------------------------------------- /tests/cases/inst/include04.rapt: -------------------------------------------------------------------------------- 1 | INCLUDE include.include01 2 | -------------------------------------------------------------------------------- /tests/cases/inst/run03.rapt: -------------------------------------------------------------------------------- 1 | RUN /bin/sh -c "echo 'foo'" 2 | -------------------------------------------------------------------------------- /tests/cases/error/dummy.rapt: -------------------------------------------------------------------------------- 1 | # this file intentionally blank 2 | -------------------------------------------------------------------------------- /tests/cases/inst/expr02.rapt: -------------------------------------------------------------------------------- 1 | RENDER foo bar a=[1,2,3] b=[4,5,6] 2 | -------------------------------------------------------------------------------- /tests/cases/inst/expr04.rapt: -------------------------------------------------------------------------------- 1 | RENDER foo bar a={"foo": "bar"} 2 | -------------------------------------------------------------------------------- /tests/cases/inst/render01.rapt: -------------------------------------------------------------------------------- 1 | RENDER include/template01.tmpl /a 2 | -------------------------------------------------------------------------------- /tests/cases/inst/render03.rapt: -------------------------------------------------------------------------------- 1 | INCLUDE render03 what="world" 2 | -------------------------------------------------------------------------------- /tests/cases/error/error_missing_include.rinc: -------------------------------------------------------------------------------- 1 | error_missing_include.rapt -------------------------------------------------------------------------------- /tests/cases/func/data1.yaml: -------------------------------------------------------------------------------- 1 | what: "hostname" 2 | where: /etc/hostname 3 | -------------------------------------------------------------------------------- /tests/cases/func/write_dict.rapt: -------------------------------------------------------------------------------- 1 | WRITE "{{data.what}}" {{data.where}} 2 | -------------------------------------------------------------------------------- /tests/cases/inst/entrypoint02.rapt: -------------------------------------------------------------------------------- 1 | ENTRYPOINT /bin/sh -c "echo foo" 2 | -------------------------------------------------------------------------------- /tests/cases/inst/write02.rapt: -------------------------------------------------------------------------------- 1 | WRITE --chown user:group "bar" /foo 2 | -------------------------------------------------------------------------------- /src/make/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod maker; 2 | pub mod parser; 3 | pub mod planner; 4 | -------------------------------------------------------------------------------- /tests/cases/inst/render03.rinc: -------------------------------------------------------------------------------- 1 | RENDER include/template02.tmpl /a what=what 2 | -------------------------------------------------------------------------------- /tests/cases/inst/mkdir03.rapt: -------------------------------------------------------------------------------- 1 | MKDIR -p --chown user:group --chmod 0755 /foo/bar 2 | -------------------------------------------------------------------------------- /tests/cases/inst/render02.rapt: -------------------------------------------------------------------------------- 1 | RENDER include/template02.tmpl /a what="world" 2 | -------------------------------------------------------------------------------- /crates/falcon/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-musl" 3 | -------------------------------------------------------------------------------- /crates/falcon/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod error; 3 | pub mod umask_proc; 4 | -------------------------------------------------------------------------------- /tests/cases/error/error_invalid_jinja.rapt: -------------------------------------------------------------------------------- 1 | # line 1 2 | $ for x inx 3 | # line 3 4 | -------------------------------------------------------------------------------- /tests/cases/error/error_failing_instruction.rapt: -------------------------------------------------------------------------------- 1 | # line 1 2 | RUN /missing 3 | # line 3 4 | -------------------------------------------------------------------------------- /tests/cases/inst/expr05.rapt: -------------------------------------------------------------------------------- 1 | RENDER foo bar a={"foo": [1,2,3], "sub": {"foo": "bar"}} 2 | -------------------------------------------------------------------------------- /src/dsl/mod.rs: -------------------------------------------------------------------------------- 1 | mod item; 2 | mod program; 3 | 4 | pub use item::*; 5 | pub use program::*; 6 | -------------------------------------------------------------------------------- /tests/cases/error/error_missing_include.rapt: -------------------------------------------------------------------------------- 1 | # line 1 2 | INCLUDE missing_file 3 | # line 3 4 | -------------------------------------------------------------------------------- /tests/cases/error/error_undefined_include_var.rapt: -------------------------------------------------------------------------------- 1 | # line 1 2 | INCLUDE dummy foo=bar 3 | # line 3 4 | -------------------------------------------------------------------------------- /tests/cases/error/error_recursive_include.rapt: -------------------------------------------------------------------------------- 1 | # line 1 2 | INCLUDE error_recursive_include 3 | # line 3 4 | -------------------------------------------------------------------------------- /tests/cases/error/error_recursive_include.rinc: -------------------------------------------------------------------------------- 1 | # line 1 2 | INCLUDE error_recursive_include 3 | # line 3 4 | -------------------------------------------------------------------------------- /book/src/images/logo-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrivers/raptor/HEAD/book/src/images/logo-title.png -------------------------------------------------------------------------------- /book/src/images/raptor-make.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrivers/raptor/HEAD/book/src/images/raptor-make.png -------------------------------------------------------------------------------- /tests/cases/error/error_missing_include_level_2.rapt: -------------------------------------------------------------------------------- 1 | # line 1 2 | INCLUDE error_missing_include 3 | # line 3 4 | -------------------------------------------------------------------------------- /book/src/images/deblive-grub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrivers/raptor/HEAD/book/src/images/deblive-grub.png -------------------------------------------------------------------------------- /tests/cases/error/error_undefined_var.rapt: -------------------------------------------------------------------------------- 1 | # line 1 2 | $ for x in missing 3 | RUN echo {{x}} 4 | $ endfor 5 | # line 3 6 | -------------------------------------------------------------------------------- /book/example/file-lister.rapt: -------------------------------------------------------------------------------- 1 | FROM docker://debian:trixie 2 | 3 | MOUNT --simple list /input 4 | 5 | CMD "ls -l /input" 6 | -------------------------------------------------------------------------------- /book/example/layers-lister.rapt: -------------------------------------------------------------------------------- 1 | FROM docker://debian:trixie 2 | 3 | MOUNT --layers input /input 4 | 5 | CMD "ls -l /input" 6 | -------------------------------------------------------------------------------- /book/src/images/raptor-circle-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrivers/raptor/HEAD/book/src/images/raptor-circle-icon.png -------------------------------------------------------------------------------- /book/example/overlay-lister.rapt: -------------------------------------------------------------------------------- 1 | FROM docker://debian:trixie 2 | 3 | MOUNT --overlay input /input 4 | 5 | CMD "ls -l /input" 6 | -------------------------------------------------------------------------------- /crates/falcon/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | mod frameio; 2 | mod protocol; 3 | 4 | pub use frameio::{FramedRead, FramedWrite}; 5 | pub use protocol::*; 6 | -------------------------------------------------------------------------------- /tests/cases/func/log.rapt: -------------------------------------------------------------------------------- 1 | $ do trace("trace") 2 | $ do debug("debug") 3 | $ do info("info") 4 | $ do warning("warning") 5 | $ do error("error") 6 | -------------------------------------------------------------------------------- /tests/cases/error/error_invalid_instruction.rapt: -------------------------------------------------------------------------------- 1 | # line 1 2 | FLORG --foo "bar" 3 | $ for x in [1,2,3,4,5] 4 | RUN ls -l 5 | $ endfor 6 | # line 4 7 | -------------------------------------------------------------------------------- /book/example/file-lister-output.rapt: -------------------------------------------------------------------------------- 1 | FROM docker://debian:trixie 2 | 3 | MOUNT --simple input /input 4 | MOUNT --file output /output 5 | 6 | CMD "ls -l /input > /output" 7 | -------------------------------------------------------------------------------- /book/example/ping.rapt: -------------------------------------------------------------------------------- 1 | FROM docker://debian:trixie 2 | 3 | RUN apt-get update 4 | RUN apt-get install -qy iputils-ping 5 | 6 | ENTRYPOINT /bin/ping 7 | 8 | CMD 8.8.8.8 9 | -------------------------------------------------------------------------------- /src/build/mod.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod cache; 3 | mod present; 4 | mod stats; 5 | 6 | pub use builder::*; 7 | pub use cache::*; 8 | pub use present::*; 9 | pub use stats::*; 10 | -------------------------------------------------------------------------------- /book/example/ssh.rapt: -------------------------------------------------------------------------------- 1 | # Start from previous layer 2 | FROM base 3 | 4 | # Update package sources, and install ssh server 5 | RUN apt-get update 6 | RUN apt-get install -qy openssh-server 7 | -------------------------------------------------------------------------------- /crates/dregistry/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod authparse; 3 | pub mod client; 4 | pub mod digest; 5 | pub mod downloader; 6 | pub mod error; 7 | pub mod reference; 8 | pub mod source; 9 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | mod location; 2 | mod safe_parent; 3 | 4 | pub use location::Location; 5 | pub use safe_parent::{SafeParent, SafeParentError}; 6 | 7 | pub mod module_name; 8 | -------------------------------------------------------------------------------- /tests/cases/func/write_arg.rapt: -------------------------------------------------------------------------------- 1 | # build dictionary from incoming arguments 2 | $ set data = {"what": what, "where": where} 3 | 4 | # then pass this on to write_dict: 5 | INCLUDE write_dict.rapt data 6 | -------------------------------------------------------------------------------- /src/sandbox/mod.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod ext; 3 | mod file; 4 | mod nspawn; 5 | mod sandbox; 6 | 7 | pub use client::*; 8 | pub use ext::*; 9 | pub use file::*; 10 | pub use nspawn::*; 11 | pub use sandbox::*; 12 | -------------------------------------------------------------------------------- /src/program/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod executor; 3 | mod loader; 4 | mod printer; 5 | mod resolve; 6 | 7 | pub use error::*; 8 | pub use executor::*; 9 | pub use loader::*; 10 | pub use printer::*; 11 | pub use resolve::*; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor backup files 2 | *~ 3 | 4 | # Rust build dir 5 | /target/ 6 | 7 | # Raptor outputs 8 | /tests/output/ 9 | /layers/ 10 | /cache*/ 11 | 12 | # Development notes and files 13 | /notes/ 14 | /rbuild/ 15 | /.emacs.* 16 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod ast; 2 | pub mod error; 3 | pub mod lexer; 4 | pub mod parser; 5 | pub mod print; 6 | pub mod util; 7 | 8 | pub use error::ParseError; 9 | 10 | pub type ParseResult = Result; 11 | -------------------------------------------------------------------------------- /tests/cases/error/error_undefined_include_var.out: -------------------------------------------------------------------------------- 1 | error: Script Error 2 | --> tests/cases/error/error_undefined_include_var.rapt:1:19 3 | | 4 | 1 | INCLUDE dummy foo=bar 5 | | ^^^ Undefined variable: bar 6 | 2 | 7 | | 8 | -------------------------------------------------------------------------------- /tests/cases/error/error_invalid_instruction.out: -------------------------------------------------------------------------------- 1 | error: Parse error 2 | --> tests/cases/error/error_invalid_instruction.rapt:1:1 3 | | 4 | 1 | FLORG --foo "bar" 5 | | ^^^^^ Expected statement 6 | 2 | RUN ls -l 7 | 3 | RUN ls -l 8 | 4 | RUN ls -l 9 | | 10 | -------------------------------------------------------------------------------- /tests/cases/error/error_failing_instruction.out: -------------------------------------------------------------------------------- 1 | error: Error while executing instruction 2 | --> tests/cases/error/error_failing_instruction.rapt:1:1 3 | | 4 | 1 | RUN /missing 5 | | ^^^^^^^^^^^^ Sandbox request error: Sandbox error: ENOENT: No such file or directory 6 | 2 | 7 | | 8 | -------------------------------------------------------------------------------- /tests/cases/error/error_missing_from.out: -------------------------------------------------------------------------------- 1 | error: Error while loading source file 2 | --> tests/cases/error/error_missing_from.rapt:1:1 3 | | 4 | 1 | FROM missingbase 5 | | ^^^^^^^^^^^^^^^^ Could not open [tests/cases/error/missingbase.rapt]: No such file or directory (os error 2) 6 | 2 | 7 | | 8 | -------------------------------------------------------------------------------- /tests/cases/error/error_missing_include.out: -------------------------------------------------------------------------------- 1 | error: Error while loading source file 2 | --> tests/cases/error/error_missing_include.rapt:1:1 3 | | 4 | 1 | INCLUDE missing_file 5 | | ^^^^^^^^^^^^^^^^^^^^ Could not open [tests/cases/error/missing_file.rinc]: No such file or directory (os error 2) 6 | 2 | 7 | | 8 | -------------------------------------------------------------------------------- /tests/cases/error/error_invalid_jinja.out: -------------------------------------------------------------------------------- 1 | error: unexpected identifier, expected in 2 | --> tests/cases/error/error_invalid_jinja.rapt:2:9 3 | | 4 | 1 | # line 1 5 | 2 | $ for x inx 6 | | ^^^ syntax error: unexpected identifier, expected in (in tests/cases/error/error_invalid_jinja.rapt:2) 7 | 3 | # line 3 8 | | 9 | -------------------------------------------------------------------------------- /book/example/base.rapt: -------------------------------------------------------------------------------- 1 | # Start from a docker iso 2 | FROM docker://debian:trixie 3 | 4 | # Set root password to "raptor" 5 | RUN usermod -p "$1$GQf2tS9s$vu72NbrDtUcvvqnyAogrH0" root 6 | 7 | # Update package sources, and install packages 8 | RUN apt-get update 9 | RUN apt-get install -qy systemd-sysv live-boot linux-image-amd64 10 | -------------------------------------------------------------------------------- /tests/cases/error/error_undefined_var.out: -------------------------------------------------------------------------------- 1 | error: undefined value 2 | --> tests/cases/error/error_undefined_var.rapt:2:12 3 | | 4 | 1 | # line 1 5 | 2 | $ for x in missing 6 | | ^^^^^^^ undefined value (in tests/cases/error/error_undefined_var.rapt:2) 7 | 3 | RUN echo {{x}} 8 | 4 | $ endfor 9 | 5 | # line 3 10 | | 11 | -------------------------------------------------------------------------------- /tests/cases/func/write_yaml.rapt: -------------------------------------------------------------------------------- 1 | # load data1.yaml into "data" 2 | $ set data = load_yaml("data1.yaml") 3 | 4 | $ set other = {"foo": "bar"} 5 | 6 | # pass arguments by names 7 | INCLUDE write_arg.rapt what=data.what where=data.where 8 | 9 | # pass arguments as dict (named data on both sides) 10 | INCLUDE write_dict.rapt data=data 11 | 12 | # shorter syntax for data=data 13 | INCLUDE write_dict.rapt data 14 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/workdir.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use camino::Utf8PathBuf; 4 | 5 | use crate::print::Theme; 6 | 7 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 8 | pub struct InstWorkdir { 9 | pub dir: Utf8PathBuf, 10 | } 11 | 12 | impl Display for InstWorkdir { 13 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 14 | f.keyword("WORKDIR")?; 15 | f.dest(&self.dir) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/program/printer.rs: -------------------------------------------------------------------------------- 1 | use crate::RaptorResult; 2 | use crate::dsl::Program; 3 | 4 | #[derive(Default)] 5 | pub struct PrintExecutor {} 6 | 7 | impl PrintExecutor { 8 | #[must_use] 9 | pub const fn new() -> Self { 10 | Self {} 11 | } 12 | 13 | pub fn run(&self, program: &Program) -> RaptorResult<()> { 14 | program.traverse(&mut |stmt| { 15 | info!("{}", stmt.inst); 16 | Ok(()) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/falcon/src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(thiserror::Error, Debug)] 2 | pub enum FalconError { 3 | #[error(transparent)] 4 | IoError(#[from] std::io::Error), 5 | 6 | #[error(transparent)] 7 | BincodeDecodeError(#[from] bincode::error::DecodeError), 8 | 9 | #[error(transparent)] 10 | BincodeEncodeError(#[from] bincode::error::EncodeError), 11 | 12 | #[error(transparent)] 13 | Errno(#[from] nix::Error), 14 | } 15 | 16 | pub type FalconResult = Result; 17 | -------------------------------------------------------------------------------- /tests/cases/error/error_missing_include_level_2.out: -------------------------------------------------------------------------------- 1 | error: Error while evaluating INCLUDE 2 | --> tests/cases/error/error_missing_include_level_2.rapt:1:1 3 | | 4 | 1 | INCLUDE error_missing_include 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (included here) 6 | 2 | 7 | | 8 | error: Error while loading source file 9 | --> tests/cases/error/error_missing_include.rinc:1:1 10 | | 11 | 1 | INCLUDE missing_file 12 | | ^^^^^^^^^^^^^^^^^^^^ Could not open [tests/cases/error/missing_file.rinc]: No such file or directory (os error 2) 13 | 2 | 14 | | 15 | -------------------------------------------------------------------------------- /crates/dregistry/examples/parse-reference.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use dregistry::reference; 4 | 5 | #[derive(Parser)] 6 | struct Cli { 7 | #[arg(required = true)] 8 | names: Vec, 9 | } 10 | 11 | fn main() { 12 | let args = Cli::parse(); 13 | 14 | for name in args.names { 15 | eprintln!("Parsing [{name}]:"); 16 | let source = reference::parse(&name).unwrap(); 17 | eprintln!("{source:#?}"); 18 | 19 | let output = source.to_string(); 20 | assert_eq!(name, output); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/cmd.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use camino::Utf8Path; 4 | 5 | use crate::print::Theme; 6 | 7 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 8 | pub struct InstCmd { 9 | pub cmd: Vec, 10 | } 11 | 12 | impl Display for InstCmd { 13 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 14 | f.keyword("CMD")?; 15 | f.dest(Utf8Path::new(&self.cmd[0]))?; 16 | for arg in &self.cmd[1..] { 17 | f.src(Utf8Path::new(arg.as_str()))?; 18 | } 19 | Ok(()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/run.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use camino::Utf8Path; 4 | 5 | use crate::print::Theme; 6 | 7 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 8 | pub struct InstRun { 9 | pub run: Vec, 10 | } 11 | 12 | impl Display for InstRun { 13 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 14 | f.keyword("RUN")?; 15 | f.dest(Utf8Path::new(&self.run[0]))?; 16 | for arg in &self.run[1..] { 17 | f.src(Utf8Path::new(arg.as_str()))?; 18 | } 19 | Ok(()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /book/src/mount-types/simple.md: -------------------------------------------------------------------------------- 1 | ## Mount type `--simple` 2 | 3 | This is the default mount type. 4 | 5 | A `--simple` mount will mount a directory from the host into the 6 | container. Docker users are likely to be familiar with this concept. 7 | 8 | ### Example 9 | 10 | ~~~admonish note title="file-lister.rapt" 11 | ```raptor 12 | {{#include ../../example/file-lister.rapt}} 13 | ``` 14 | ~~~ 15 | 16 | This container can be run, to provide a file listing on the mounted directory: 17 | 18 | ```sh 19 | $ sudo raptor run file-lister -M list /tmp 20 | ... <"ls" output would be shown here> ... 21 | ``` 22 | -------------------------------------------------------------------------------- /book/src/inst/entrypoint.md: -------------------------------------------------------------------------------- 1 | # Instruction `ENTRYPOINT` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | ENTRYPOINT [...] 6 | ``` 7 | ~~~ 8 | 9 | ```admonish important title="Build-time instruction" 10 | The `ENTRYPOINT` instructions only affects *running* a container, not *building* a 11 | container. 12 | ``` 13 | 14 | This instruction sets the entrypoint for the container, which is used when 15 | running commands in it. 16 | 17 | The default value is `["/bin/sh", "-c"]`. 18 | 19 | See the [`CMD` instruction](cmd.md) for details, including the relationship 20 | between `ENTRYPOINT` and `CMD`. 21 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/entrypoint.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use camino::Utf8Path; 4 | 5 | use crate::print::Theme; 6 | 7 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 8 | pub struct InstEntrypoint { 9 | pub entrypoint: Vec, 10 | } 11 | 12 | impl Display for InstEntrypoint { 13 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 14 | f.keyword("ENTRYPOINT")?; 15 | f.dest(Utf8Path::new(&self.entrypoint[0]))?; 16 | for arg in &self.entrypoint[1..] { 17 | f.src(Utf8Path::new(arg.as_str()))?; 18 | } 19 | Ok(()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/util/location.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::ast::Origin; 4 | 5 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 6 | pub struct Location { 7 | pub origin: Origin, 8 | pub inner: T, 9 | } 10 | 11 | impl Location { 12 | pub fn origin(&self) -> Origin { 13 | self.origin.clone() 14 | } 15 | 16 | pub const fn make(origin: Origin, inner: T) -> Self { 17 | Self { origin, inner } 18 | } 19 | } 20 | 21 | impl Display for Location { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | Display::fmt(&self.inner, f) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/write.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use camino::Utf8PathBuf; 4 | 5 | use crate::ast::Chown; 6 | use crate::print::Theme; 7 | 8 | #[derive(Clone, Hash, Debug, PartialEq, Eq)] 9 | pub struct InstWrite { 10 | pub dest: Utf8PathBuf, 11 | pub body: String, 12 | pub chmod: Option, 13 | pub chown: Option, 14 | } 15 | 16 | impl Display for InstWrite { 17 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 18 | f.keyword("WRITE")?; 19 | f.chmod(&self.chmod)?; 20 | f.chown(&self.chown)?; 21 | f.dest(&self.dest)?; 22 | f.value(&self.body)?; 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::lexer::Token; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum ParseError { 5 | #[error(transparent)] 6 | LexerError(#[from] crate::lexer::LexerError), 7 | 8 | #[error(transparent)] 9 | ParseIntError(#[from] std::num::ParseIntError), 10 | 11 | #[error( 12 | "Invalid permission mask\n\nValue must specified as 3 or 4 octal digits (0755, 1777, 644, 640, etc)" 13 | )] 14 | InvalidPermissionMask, 15 | 16 | #[error("Expected {0}")] 17 | Expected(&'static str), 18 | 19 | #[error("Expected {} but found {}", .exp.description(), .found.description())] 20 | Mismatch { exp: Token, found: Token }, 21 | } 22 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/copy.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use camino::Utf8PathBuf; 4 | 5 | use crate::ast::Chown; 6 | use crate::print::Theme; 7 | 8 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 9 | pub struct InstCopy { 10 | pub srcs: Vec, 11 | pub dest: Utf8PathBuf, 12 | pub chmod: Option, 13 | pub chown: Option, 14 | } 15 | 16 | impl Display for InstCopy { 17 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 18 | f.keyword("COPY")?; 19 | f.chmod(&self.chmod)?; 20 | f.chown(&self.chown)?; 21 | for src in &self.srcs { 22 | f.src(src)?; 23 | } 24 | f.dest(&self.dest) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/mod.rs: -------------------------------------------------------------------------------- 1 | mod chown; 2 | mod cmd; 3 | mod copy; 4 | mod entrypoint; 5 | mod env; 6 | mod from; 7 | mod include; 8 | mod inst; 9 | mod mkdir; 10 | mod mount; 11 | mod origin; 12 | mod render; 13 | mod run; 14 | mod workdir; 15 | mod write; 16 | 17 | pub use chown::*; 18 | pub use cmd::*; 19 | pub use copy::*; 20 | pub use entrypoint::*; 21 | pub use env::*; 22 | pub use from::*; 23 | pub use include::*; 24 | pub use inst::*; 25 | pub use mkdir::*; 26 | pub use mount::*; 27 | pub use origin::*; 28 | pub use render::*; 29 | pub use run::*; 30 | pub use workdir::*; 31 | pub use write::*; 32 | 33 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 34 | pub struct Statement { 35 | pub inst: Instruction, 36 | pub origin: Origin, 37 | } 38 | -------------------------------------------------------------------------------- /src/template/load_yaml.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use camino::Utf8Path; 4 | use minijinja::{Environment, Error, State, Value}; 5 | 6 | use crate::template::AdaptError; 7 | 8 | #[allow(clippy::unnecessary_wraps)] 9 | pub fn load_yaml(state: &State, filename: &str) -> Result { 10 | let path = Utf8Path::new(state.name()) 11 | .parent() 12 | .adapt_err("Invalid path")?; 13 | let file = File::open(path.join(filename)).adapt_err("Failed to open file")?; 14 | let yml: serde_yml::Value = serde_yml::from_reader(&file).adapt_err("Failed to parse yaml")?; 15 | 16 | Ok(Value::from_serialize(yml)) 17 | } 18 | 19 | pub fn add_functions(env: &mut Environment) { 20 | env.add_function("load_yaml", load_yaml); 21 | } 22 | -------------------------------------------------------------------------------- /book/src/inst/mkdir.md: -------------------------------------------------------------------------------- 1 | # Instruction `MKDIR` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | MKDIR [] 6 | ``` 7 | ~~~ 8 | 9 | ```admonish tip 10 | See the section on [file options](/file-options.md). 11 | ``` 12 | 13 | The `MKDIR` instruction creates an empty directory inside the build target. 14 | 15 | This is roughly equivalent to the following command: 16 | 17 | ```raptor 18 | RUN mkdir /foo 19 | ``` 20 | 21 | However, using `RUN mkdir` requires the `mkdir` command to be available and 22 | executable inside the build target. This is not always the case, especially when 23 | building things from scratch. 24 | 25 | ## Example 26 | 27 | ```raptor 28 | MKDIR /data 29 | 30 | MKDIR -p --chown babelfish:daemon /usr/local/translate/ 31 | ``` 32 | -------------------------------------------------------------------------------- /book/src/images/raptor-layers.pikchr: -------------------------------------------------------------------------------- 1 | # scale = 0.8 2 | fill = white 3 | 4 | All: [ 5 | 6 | [ 7 | file 8 | file at previous + (0.1,-0.1) 9 | file at previous + (0.1,-0.1) "Raptor" "source" "code" 10 | ] 11 | 12 | arrow "Raptor build" above width 1 13 | 14 | Layers: [ 15 | file 16 | file at previous + (0.1,-0.1) 17 | file at previous + (0.1,-0.1) "Layers" 18 | ] 19 | 20 | #cylinder \ 21 | # wid Layers.width + 0.2 \ 22 | # ht Layers.height + 0.4 \ 23 | # at Layers \ 24 | # fill 0xc6e2ff \ 25 | # behind Layers 26 | 27 | #text "Disk storage" at previous.t + (0, 0.1) 28 | ] 29 | 30 | box \ 31 | wid All.width + 0.2 \ 32 | ht All.height + 0.2 \ 33 | at All \ 34 | fill 0xd0f0ff \ 35 | behind All 36 | # fill 0xc6e2ff \ 37 | -------------------------------------------------------------------------------- /crates/falcon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "falcon" 3 | version = "0.1.0" 4 | description = "Worker spawned by raptor, to execute instructions inside namespaces" 5 | edition.workspace = true 6 | license.workspace = true 7 | readme.workspace = true 8 | categories.workspace = true 9 | repository.workspace = true 10 | keywords.workspace = true 11 | authors.workspace = true 12 | 13 | [dependencies] 14 | bincode = { workspace = true, features = ["serde"] } 15 | camino = { workspace = true, features = ["serde1"] } 16 | colog = { workspace = true } 17 | log = { workspace = true } 18 | nix = { workspace = true, features = ["fs", "user"] } 19 | serde = { workspace = true, features = ["derive"] } 20 | thiserror = { workspace = true } 21 | 22 | [lints] 23 | workspace = true 24 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/mkdir.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use camino::Utf8PathBuf; 4 | use colored::Colorize; 5 | 6 | use crate::ast::Chown; 7 | use crate::print::Theme; 8 | 9 | #[derive(Clone, Hash, Debug, PartialEq, Eq)] 10 | pub struct InstMkdir { 11 | pub dest: Utf8PathBuf, 12 | pub chmod: Option, 13 | pub chown: Option, 14 | pub parents: bool, 15 | } 16 | 17 | impl Display for InstMkdir { 18 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 19 | f.keyword("MKDIR")?; 20 | if self.parents { 21 | write!(f, "{}", " -p".bright_white())?; 22 | } 23 | f.chmod(&self.chmod)?; 24 | f.chown(&self.chown)?; 25 | f.dest(&self.dest)?; 26 | Ok(()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/template/header.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use minijinja::Environment; 3 | 4 | #[allow(clippy::needless_pass_by_value)] 5 | pub fn header(comment_start: Option<&str>, comment_end: Option<&str>) -> String { 6 | let a = comment_start.unwrap_or("## "); 7 | let b = comment_end.unwrap_or(""); 8 | 9 | [ 10 | "--------------------------------------", 11 | "Generated by Raptor ", 12 | "", 13 | "THIS FILE IS AUTOMATICALLY GENERATED.", 14 | "DO NOT EDIT. ALL CHANGES WILL BE LOST.", 15 | "--------------------------------------", 16 | ] 17 | .iter() 18 | .map(|line| format!("{a}{line}{b}")) 19 | .join("\n") 20 | } 21 | 22 | pub fn add_functions(env: &mut Environment) { 23 | env.add_function("header", header); 24 | } 25 | -------------------------------------------------------------------------------- /book/src/inst/render.md: -------------------------------------------------------------------------------- 1 | # Instruction `RENDER` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | RENDER [] [...] 6 | ``` 7 | ~~~ 8 | 9 | The `RENDER` instruction renders a file from a template, and writes it to the 10 | specified destination. It accepts the same `key=value` arguments as 11 | `INCLUDE`. These arguments are made available in the template. 12 | 13 | Example: 14 | 15 | ```raptor 16 | RENDER widgetfactory.tmpl /etc/widgetd/server.conf host="example.org" port=1234 17 | ``` 18 | 19 | The short form `name` (meaning `name=name`) is also supported here. 20 | 21 | For example, in a component where `host` and `port` are available in the 22 | environment: 23 | 24 | ```raptor 25 | RENDER widgetfactory.tmpl /etc/widgetd/server.conf host port 26 | ``` 27 | -------------------------------------------------------------------------------- /src/util/kwargs.rs: -------------------------------------------------------------------------------- 1 | use minijinja::Error; 2 | use minijinja::value::{ArgType, Kwargs}; 3 | 4 | pub trait KwargsExt { 5 | fn get_option<'a, T>(&'a self, name: &'a str) -> Result, Error> 6 | where 7 | T: ArgType<'a, Output = T>; 8 | 9 | fn get_or_default<'a, T>(&'a self, name: &'a str, default: T) -> Result 10 | where 11 | T: ArgType<'a, Output = T>, 12 | { 13 | Ok(self.get_option(name)?.unwrap_or(default)) 14 | } 15 | } 16 | 17 | impl KwargsExt for Kwargs { 18 | fn get_option<'a, T>(&'a self, name: &'a str) -> Result, Error> 19 | where 20 | T: ArgType<'a, Output = T>, 21 | { 22 | if self.has(name) { 23 | self.get(name) 24 | } else { 25 | Ok(None) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/util/tty.rs: -------------------------------------------------------------------------------- 1 | use std::os::fd::AsRawFd; 2 | 3 | use nix::errno::Errno; 4 | use nix::libc::{TIOCSWINSZ, ioctl, winsize}; 5 | 6 | pub trait TtyIoctl: AsRawFd { 7 | fn tty_set_window_size(&self, ws_size: winsize) -> Result<(), std::io::Error>; 8 | 9 | fn tty_set_size(&self, rows: u16, cols: u16) -> Result<(), std::io::Error> { 10 | self.tty_set_window_size(winsize { 11 | ws_row: rows, 12 | ws_col: cols, 13 | ws_xpixel: 0, 14 | ws_ypixel: 0, 15 | }) 16 | } 17 | } 18 | 19 | impl TtyIoctl for T { 20 | fn tty_set_window_size(&self, ws_size: winsize) -> Result<(), std::io::Error> { 21 | let res = unsafe { ioctl(self.as_raw_fd(), TIOCSWINSZ, &ws_size) }; 22 | 23 | Ok(Errno::result(res).map(drop)?) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/util/safe_parent.rs: -------------------------------------------------------------------------------- 1 | use camino::{Utf8Path, Utf8PathBuf}; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum SafeParentError { 5 | #[error("Cannot get parent path from {0:?}")] 6 | BadPathNoParent(Utf8PathBuf), 7 | } 8 | 9 | pub trait SafeParent { 10 | fn try_parent(&self) -> Result<&Utf8Path, SafeParentError>; 11 | } 12 | 13 | impl SafeParent for Utf8Path { 14 | fn try_parent(&self) -> Result<&Utf8Path, SafeParentError> { 15 | self.parent() 16 | .ok_or_else(|| SafeParentError::BadPathNoParent(self.into())) 17 | } 18 | } 19 | 20 | impl SafeParent for Utf8PathBuf { 21 | fn try_parent(&self) -> Result<&Utf8Path, SafeParentError> { 22 | self.parent() 23 | .ok_or_else(|| SafeParentError::BadPathNoParent(self.into())) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/raptor-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raptor-parser" 3 | version = "0.1.0" 4 | description = "Parser for Raptor build files (.rapt & .rinc)" 5 | 6 | edition.workspace = true 7 | license.workspace = true 8 | readme.workspace = true 9 | categories.workspace = true 10 | repository.workspace = true 11 | keywords.workspace = true 12 | authors.workspace = true 13 | 14 | [dependencies] 15 | thiserror = { workspace = true } 16 | camino = { workspace = true } 17 | serde = { workspace = true } 18 | colored = { workspace = true } 19 | minijinja = { workspace = true } 20 | logos = { workspace = true, features = ["forbid_unsafe"] } 21 | 22 | [lints] 23 | workspace = true 24 | 25 | [dev-dependencies] 26 | colog.workspace = true 27 | log.workspace = true 28 | pretty_assertions.workspace = true 29 | serde_json.workspace = true 30 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/render.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use camino::Utf8PathBuf; 4 | 5 | use crate::ast::{Chown, IncludeArg}; 6 | use crate::print::Theme; 7 | 8 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 9 | pub struct InstRender { 10 | pub src: Utf8PathBuf, 11 | pub dest: Utf8PathBuf, 12 | pub chmod: Option, 13 | pub chown: Option, 14 | pub args: Vec, 15 | } 16 | 17 | impl Display for InstRender { 18 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 19 | f.keyword("RENDER")?; 20 | f.chmod(&self.chmod)?; 21 | f.chown(&self.chown)?; 22 | f.src(&self.src)?; 23 | f.dest(&self.dest)?; 24 | for arg in &self.args { 25 | f.include_arg(arg)?; 26 | } 27 | Ok(()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/util/clapcolor.rs: -------------------------------------------------------------------------------- 1 | use anstyle::{AnsiColor, Color, Style}; 2 | 3 | #[must_use] 4 | pub const fn style() -> clap::builder::Styles { 5 | let style = Style::new(); 6 | let fg_cyan = style.fg_color(Some(Color::Ansi(AnsiColor::Cyan))); 7 | let fg_green = style.fg_color(Some(Color::Ansi(AnsiColor::Green))); 8 | let fg_red = style.fg_color(Some(Color::Ansi(AnsiColor::Red))); 9 | let fg_white = style.fg_color(Some(Color::Ansi(AnsiColor::White))); 10 | let fg_yellow = style.fg_color(Some(Color::Ansi(AnsiColor::Yellow))); 11 | 12 | clap::builder::Styles::styled() 13 | .usage(fg_white.bold()) 14 | .header(fg_yellow.bold()) 15 | .literal(fg_green) 16 | .invalid(fg_red.bold()) 17 | .error(fg_red.bold()) 18 | .valid(fg_green.bold()) 19 | .placeholder(fg_cyan) 20 | } 21 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/from.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Debug, Display}; 2 | 3 | use crate::print::Theme; 4 | use crate::util::module_name::ModuleName; 5 | 6 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 7 | pub enum FromSource { 8 | Raptor(ModuleName), 9 | Docker(String), 10 | } 11 | 12 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 13 | pub struct InstFrom { 14 | pub from: FromSource, 15 | } 16 | 17 | impl Display for InstFrom { 18 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 19 | f.keyword("FROM")?; 20 | f.from(&self.from) 21 | } 22 | } 23 | 24 | impl Display for FromSource { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | match self { 27 | Self::Raptor(src) => write!(f, "{src}"), 28 | Self::Docker(src) => write!(f, "docker://{src}"), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/env.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::print::Theme; 4 | 5 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 6 | pub struct InstEnvAssign { 7 | pub key: String, 8 | pub value: String, 9 | } 10 | 11 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 12 | pub struct InstEnv { 13 | pub env: Vec, 14 | } 15 | 16 | impl InstEnvAssign { 17 | pub fn new(key: impl AsRef, value: impl AsRef) -> Self { 18 | Self { 19 | key: key.as_ref().to_string(), 20 | value: value.as_ref().to_string(), 21 | } 22 | } 23 | } 24 | 25 | impl Display for InstEnv { 26 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 27 | f.keyword("ENV")?; 28 | for env in &self.env { 29 | f.env_arg(env)?; 30 | } 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Christian Iversen"] 3 | language = "en" 4 | src = "src" 5 | title = "Raptor Manual" 6 | 7 | [build] 8 | create-missing = false 9 | 10 | [preprocessor.pikchr] 11 | 12 | [preprocessor.mermaid] 13 | command = "mdbook-mermaid" 14 | 15 | [preprocessor.admonish] 16 | command = "mdbook-admonish" 17 | assets_version = "3.1.0" # do not edit: managed by `mdbook-admonish install` 18 | 19 | [output.html] 20 | default-theme = "ayu" 21 | preferred-dark-theme = "ayu" 22 | smart-punctuation = true 23 | additional-css = ["./mdbook-admonish.css"] 24 | git-repository-url = "https://github.com/chrivers/raptor" 25 | hash-files = true 26 | #additional-js = ["mermaid.min.js", "mermaid-init.js"] 27 | 28 | # to build custom highlight.js: 29 | # 30 | # node ./tools/build.js -t browser plaintext dockerfile raptorfile rust diff json yaml ini bnf 31 | # 32 | # cp build/highlight.min.js ../theme/highlight.js 33 | -------------------------------------------------------------------------------- /book/src/inst/run.md: -------------------------------------------------------------------------------- 1 | # Instruction `RUN` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | RUN [...] 6 | ``` 7 | ~~~ 8 | 9 | The `RUN` instruction executes the given command inside the build namespace. 10 | 11 | ```raptor 12 | # enable the foo service 13 | RUN systemctl enable foo.service 14 | ``` 15 | 16 | ~~~admonish warning title="Important" 17 | Arguments are executed as-is, i.e. without shell expansion, redirection, 18 | piping, etc. 19 | 20 | This ensures full control over the parsing of commands, but it 21 | also means normal shell syntax is not available: 22 | 23 | ```raptor 24 | # BROKEN: This will call "cat" with 3 arguments 25 | RUN cat /etc/hostname "|" md5sum 26 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this will not work 27 | ``` 28 | ~~~ 29 | 30 | Instead, `/bin/sh` can be called explicitly: 31 | 32 | ```raptor 33 | # This will produce the md5sum of /etc/hostname 34 | RUN /bin/sh -c "cat /etc/hostname | md5sum" 35 | ``` 36 | -------------------------------------------------------------------------------- /book/src/inst/env.md: -------------------------------------------------------------------------------- 1 | # Instruction `ENV` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | ENV = [...] 6 | ``` 7 | ~~~ 8 | 9 | The `ENV` instruction sets one or more environment variables inside the build namespace. 10 | 11 | ```admonish important 12 | The `ENV` instructions only affects *building* a container, not *running* a 13 | container. 14 | ``` 15 | 16 | Example: 17 | 18 | ```raptor 19 | ENV CFLAGS="--with-sprinkles" 20 | ENV API_TOKEN="acbd18db4cc2f85cedef654fccc4a4d8" API_USER="user@example.org" 21 | ``` 22 | 23 | The `ENV` instruction affects all instructions that come after it, in the same layer. 24 | 25 | It is possible to overwrite a value that has been set previously: 26 | 27 | ```raptor 28 | FROM docker://busybox 29 | 30 | ENV HITCHHIKER="Arthur Dent" 31 | RUN sh -c "echo $HITCHHIKER" # outputs "Arthur Dent" 32 | 33 | ENV HITCHHIKER="Ford Prefect" 34 | RUN sh -c "echo $HITCHHIKER" # outputs "Ford Prefect" 35 | ``` 36 | -------------------------------------------------------------------------------- /crates/falcon/src/bin/falcon-test.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::net::UnixListener; 2 | 3 | use falcon::client::{FramedRead, FramedWrite, Request, RequestRun, Response}; 4 | use falcon::error::FalconResult; 5 | use log::{error, info}; 6 | 7 | fn main() -> FalconResult<()> { 8 | colog::init(); 9 | let socket_name = std::env::var("FALCON_SOCKET").expect("Must have FALCON_SOCKET set"); 10 | let listen = UnixListener::bind(socket_name)?; 11 | 12 | let (mut stream, _addr) = listen.accept()?; 13 | 14 | let req = Request::Run(RequestRun { 15 | arg0: String::from("sh"), 16 | argv: ["/bin/sh", "-c", "id"] 17 | .into_iter() 18 | .map(ToOwned::to_owned) 19 | .collect(), 20 | }); 21 | 22 | info!("writing frame: {req:?}"); 23 | stream.write_framed(req)?; 24 | 25 | let resp: Response = stream.read_framed()?; 26 | 27 | match resp { 28 | Err(err) => error!("Error: {err}"), 29 | Ok(res) => info!("Success: {res}"), 30 | } 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/util/flag.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | 3 | pub struct Flag(AtomicBool); 4 | 5 | impl Flag { 6 | #[must_use] 7 | pub const fn new(value: bool) -> Self { 8 | Self(AtomicBool::new(value)) 9 | } 10 | 11 | #[must_use] 12 | pub fn get(&self) -> bool { 13 | self.0.load(Ordering::Relaxed) 14 | } 15 | 16 | pub fn set(&self, value: bool) { 17 | self.0.store(value, Ordering::Relaxed); 18 | } 19 | } 20 | 21 | #[allow(clippy::bool_assert_comparison)] 22 | #[cfg(test)] 23 | mod tests { 24 | use crate::util::flag::Flag; 25 | 26 | #[test] 27 | fn flag_new() { 28 | let flag = Flag::new(false); 29 | assert_eq!(flag.get(), false); 30 | 31 | let flag = Flag::new(true); 32 | assert_eq!(flag.get(), true); 33 | } 34 | 35 | #[test] 36 | fn flag_set() { 37 | let flag = Flag::new(false); 38 | assert_eq!(flag.get(), false); 39 | flag.set(true); 40 | assert_eq!(flag.get(), true); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/tui/jobstate.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::{Color, Style}; 2 | use throbber_widgets_tui::QUADRANT_BLOCK_CRACK; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 5 | pub enum JobState { 6 | Planned, 7 | Running, 8 | Completed, 9 | Failed, 10 | } 11 | 12 | impl JobState { 13 | #[must_use] 14 | pub const fn symbol(self, index: usize) -> &'static str { 15 | let table = QUADRANT_BLOCK_CRACK; 16 | match self { 17 | Self::Planned => "✚", 18 | Self::Running => table.symbols[index % table.symbols.len()], 19 | Self::Completed => "🗹", 20 | Self::Failed => "X", 21 | } 22 | } 23 | 24 | #[must_use] 25 | pub const fn color(self) -> Style { 26 | match self { 27 | Self::Planned => Style::new().fg(Color::White), 28 | Self::Running => Style::new().fg(Color::LightBlue), 29 | Self::Completed => Style::new().fg(Color::LightGreen), 30 | Self::Failed => Style::new().fg(Color::LightRed), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /book/src/inst/workdir.md: -------------------------------------------------------------------------------- 1 | # Instruction `WORKDIR` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | WORKDIR 6 | ``` 7 | ~~~ 8 | 9 | ```admonish important 10 | The `WORKDIR` instructions only affects *building* a container, not *running* 11 | a container. 12 | ``` 13 | 14 | The `WORKDIR` instruction changes the current working directory inside the build 15 | namespace. This affects all subsequent relative paths, including the `RUN` 16 | instruction. 17 | 18 | The workdir is **not** inherited through `FROM`. 19 | 20 | The initial workdir is always `/`. 21 | 22 | ## Example 23 | 24 | ```raptor 25 | # This will copy "program" to "/bin/program" (since initial directory is "/") 26 | COPY program bin/program 27 | 28 | # This creates /foo 29 | RUN /bin/sh -c "touch foo" 30 | 31 | 32 | 33 | # Switch to /usr 34 | WORKDIR /usr 35 | 36 | # The same command will now copy "program" to "/usr/bin/program" 37 | COPY program bin/program 38 | 39 | 40 | 41 | # Switch to /tmp 42 | WORKDIR /tmp 43 | 44 | # This creates /tmp/foo 45 | RUN /bin/sh -c "touch foo" 46 | ``` 47 | -------------------------------------------------------------------------------- /book/src/syntax.md: -------------------------------------------------------------------------------- 1 | | Instruction | Multiple valid? | Build or Run | 2 | |:-----------------------------------|:----------------|:-------------| 3 | | [`FROM`](inst/from.md) | ***No*** | Build | 4 | | [`RUN`](inst/run.md) | Yes | Build | 5 | | [`ENV`](inst/env.md) | Yes | Build | 6 | | [`WORKDIR`](inst/workdir.md) | Yes | Build | 7 | | [`WRITE`](inst/write.md) | Yes | Build | 8 | | [`MKDIR`](inst/mkdir.md) | Yes | Build | 9 | | [`COPY`](inst/copy.md) | Yes | Build | 10 | | [`INCLUDE`](inst/include.md) | Yes | Build | 11 | | [`RENDER`](inst/render.md) | Yes | Build | 12 | | [`MOUNT`](inst/mount.md) | Yes | Run | 13 | | [`ENTRYPOINT`](inst/entrypoint.md) | ***No*** | Run | 14 | | [`CMD`](inst/cmd.md) | ***No*** | Run | 15 | -------------------------------------------------------------------------------- /crates/raptor-parser/examples/parse.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::sync::Arc; 3 | 4 | use log::error; 5 | use logos::Logos; 6 | use raptor_parser::ParseError; 7 | use raptor_parser::lexer::Token; 8 | use raptor_parser::parser::Parser; 9 | 10 | #[derive(thiserror::Error, Debug)] 11 | pub enum Error { 12 | #[error(transparent)] 13 | IoError(#[from] std::io::Error), 14 | 15 | #[error(transparent)] 16 | ParseError(#[from] ParseError), 17 | } 18 | 19 | type Result = std::result::Result; 20 | 21 | fn parse(buf: &str) -> Result<()> { 22 | let lexer = Token::lexer(buf); 23 | let mut parser = Parser::new(lexer, Arc::new("".into())); 24 | 25 | for stmt in parser.file()? { 26 | println!("{}", stmt.inst); 27 | } 28 | 29 | Ok(()) 30 | } 31 | 32 | fn main() -> Result<()> { 33 | colog::init(); 34 | 35 | let mut buf = String::new(); 36 | std::io::stdin().read_to_string(&mut buf)?; 37 | 38 | if let Err(err) = parse(&buf) { 39 | error!("Parse failed: {err}"); 40 | } 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/origin.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::ops::Range; 3 | use std::sync::Arc; 4 | 5 | use camino::{Utf8Path, Utf8PathBuf}; 6 | 7 | use crate::util::{SafeParent, SafeParentError}; 8 | 9 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 10 | pub struct Origin { 11 | pub path: Arc, 12 | pub span: Range, 13 | } 14 | 15 | impl Origin { 16 | #[must_use] 17 | pub const fn new(path: Arc, span: Range) -> Self { 18 | Self { path, span } 19 | } 20 | 21 | pub fn make(path: impl AsRef, span: Range) -> Self { 22 | Self::new(Arc::new(path.as_ref().into()), span) 23 | } 24 | 25 | #[must_use] 26 | pub fn inline() -> Self { 27 | Self::make("", 0..0) 28 | } 29 | 30 | pub fn basedir(&self) -> Result<&Utf8Path, SafeParentError> { 31 | self.path.try_parent() 32 | } 33 | 34 | pub fn path_for(&self, path: &Utf8Path) -> Result { 35 | Ok(self.basedir()?.join(path)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /book/src/inst/write.md: -------------------------------------------------------------------------------- 1 | # Instruction `WRITE` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | WRITE [] 6 | ``` 7 | ~~~ 8 | 9 | ```admonish tip 10 | See the section on [file options](/file-options.md). 11 | ``` 12 | 13 | The `WRITE` instruction writes a fixed string to the given path. 14 | 15 | A file can be added to the build output with `COPY`, but sometimes we just need 16 | to write a short value, and `COPY` might feel like overkill. 17 | 18 | Using `WRITE`, we can put values directly into files: 19 | 20 | ```raptor 21 | WRITE "hello world" hello.txt 22 | ``` 23 | 24 | ~~~admonish tip 25 | Be aware that `WRITE` does not add a newline at the end of your input. 26 | 27 | For text files, it is almost always preferred to end with a newline. 28 | 29 | To do this, add `\n` at the end of the quoted string: 30 | 31 | ```raptor 32 | WRITE "hostname\n" /etc/hostname 33 | ``` 34 | ~~~ 35 | 36 | The same file options as `COPY` and `RENDER` are accepted: 37 | 38 | ```raptor 39 | WRITE --chmod 0600 --chown service:root "API-TOKEN" /etc/service/token.conf 40 | ``` 41 | -------------------------------------------------------------------------------- /book/src/inst/copy.md: -------------------------------------------------------------------------------- 1 | # Instruction `COPY` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | COPY [] [...] 6 | ``` 7 | ~~~ 8 | 9 | ```admonish tip 10 | See the section on [file options](/file-options.md). 11 | ``` 12 | 13 | The `COPY` instruction takes one or more source files, and copies them to the 14 | destination. 15 | 16 | If multiple source files are specified, the destination MUST BE a directory. 17 | 18 | | Input | Destination | Result | 19 | |:---------------|:------------|:---------------------------------------------------------| 20 | | Single file | File | File written with destination filename | 21 | | Single file | Directory | File written to destination dir, with source filename | 22 | | Multiple files | File | ***Error*** | 23 | | Multiple files | Directory | Files written to destination dir, with original filename | 24 | | Directory | Any | ***Not yet supported*** | 25 | -------------------------------------------------------------------------------- /book/raptorfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | Language: Raptorfile 3 | Requires: bash.js 4 | Author: Alexis Hénaut 5 | Author: Christian Iversen 6 | Description: language definition for Raptor files 7 | Website: https://github.com/chrivers/raptor 8 | Category: config 9 | */ 10 | 11 | /** @type LanguageFn */ 12 | export default function(hljs) { 13 | const KEYWORDS = [ 14 | "FROM", 15 | "ENV", 16 | "MOUNT", 17 | ]; 18 | return { 19 | name: 'raptorfile', 20 | aliases: [ 'raptor' ], 21 | case_insensitive: false, 22 | keywords: KEYWORDS, 23 | contains: [ 24 | hljs.HASH_COMMENT_MODE, 25 | hljs.APOS_STRING_MODE, 26 | hljs.QUOTE_STRING_MODE, 27 | hljs.NUMBER_MODE, 28 | { 29 | beginKeywords: 'RENDER WRITE MKDIR COPY INCLUDE RUN WORKDIR ENTRYPOINT CMD', 30 | starts: { 31 | end: /[^\\]$/, 32 | subLanguage: 'bash' 33 | } 34 | } 35 | ], 36 | illegal: '(&mut self) -> FalconResult { 10 | let mut len_bytes: [u8; 4] = [0; 4]; 11 | self.read_exact(&mut len_bytes)?; 12 | let len = u32::from_be_bytes(len_bytes); 13 | 14 | let mut req = vec![0; len as usize]; 15 | self.read_exact(&mut req)?; 16 | 17 | Ok(bincode::serde::decode_from_slice(&req, bincode::config::standard())?.0) 18 | } 19 | } 20 | 21 | pub trait FramedWrite: Write { 22 | #[allow(clippy::cast_possible_truncation)] 23 | fn write_framed(&mut self, value: impl Serialize) -> FalconResult<()> { 24 | let buf = bincode::serde::encode_to_vec(&value, bincode::config::standard())?; 25 | let len_bytes = (buf.len() as u32).to_be_bytes(); 26 | self.write_all(&len_bytes)?; 27 | self.write_all(&buf)?; 28 | 29 | Ok(()) 30 | } 31 | } 32 | 33 | impl FramedRead for R {} 34 | impl FramedWrite for W {} 35 | -------------------------------------------------------------------------------- /book/src/mount-types/file.md: -------------------------------------------------------------------------------- 1 | ## Mount type `--file` 2 | 3 | This mount type requires that a single file is mounted. 4 | 5 | ~~~admonish tip 6 | When running a raptor container with a `--file` mount, the target file will be created if it does not exist. 7 | ~~~ 8 | 9 | ### Example 10 | 11 | Let us expand on the earlier example, to make the file lister provide output to a file. 12 | 13 | ~~~admonish note title="file-lister-output.rapt" 14 | ```raptor 15 | {{#include ../../example/file-lister-output.rapt}} 16 | ``` 17 | ~~~ 18 | 19 | Now that we have named the mounts `input` and `output`, we can use the 20 | [shorthand notation](../inst/mount.md#admonition-tip-1) for convenience: 21 | 22 | ```sh 23 | $ sudo raptor run file-lister-output -I /etc -O /tmp/filelist.txt 24 | ... 25 | $ sudo cat /tmp/filelist.txt 26 | ... <"ls" output would be shown here> ... 27 | ``` 28 | 29 | The above example would generate a file listing of `/etc` **from the host**, and 30 | place it in `/tmp/filelist.txt`. However, the execution of `ls` takes place in 31 | the container. 32 | 33 | For example, multiple targets in the [raptor builders](../builders/index.md) 34 | project uses this mount type for output files. 35 | -------------------------------------------------------------------------------- /crates/raptor-parser/examples/show-lexing.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::io::Write; 3 | 4 | use colored::Colorize; 5 | use log::error; 6 | use logos::Logos; 7 | 8 | use raptor_parser::lexer::Token; 9 | 10 | fn main() -> Result<(), std::io::Error> { 11 | colog::init(); 12 | 13 | let mut buf = String::new(); 14 | std::io::stdin().read_to_string(&mut buf)?; 15 | 16 | let mut lexer = Token::lexer(&buf); 17 | 18 | let mut stdout = std::io::stdout().lock(); 19 | 20 | while let Some(token) = lexer.next() { 21 | match token { 22 | Ok(Token::Bareword) => write!(stdout, "{}", lexer.slice().bright_white())?, 23 | Ok(Token::Newline | Token::Whitespace) => { 24 | write!(stdout, "{}", lexer.slice())?; 25 | } 26 | Ok(Token::String(txt)) => write!(stdout, "{}", format!("{txt:?}").yellow())?, 27 | Ok(Token::Comment) => writeln!(stdout, "{}", lexer.slice().dimmed())?, 28 | Ok(Token::Eof) => break, 29 | Ok(_) => write!(stdout, "{}", lexer.slice().purple())?, 30 | Err(err) => error!("Lexer error: {err}"), 31 | } 32 | } 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/chown.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | #[derive(Clone, Debug, Hash, Default, PartialEq, Eq)] 4 | pub struct Chown { 5 | pub user: Option, 6 | pub group: Option, 7 | } 8 | 9 | impl Chown { 10 | pub fn new(user: impl AsRef, group: impl AsRef) -> Self { 11 | Self { 12 | user: Some(user.as_ref().to_owned()), 13 | group: Some(group.as_ref().to_owned()), 14 | } 15 | } 16 | 17 | pub fn user(user: impl AsRef) -> Self { 18 | Self { 19 | user: Some(user.as_ref().to_owned()), 20 | group: None, 21 | } 22 | } 23 | 24 | pub fn group(group: impl AsRef) -> Self { 25 | Self { 26 | user: None, 27 | group: Some(group.as_ref().to_owned()), 28 | } 29 | } 30 | } 31 | 32 | impl Display for Chown { 33 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 34 | if let Some(user) = &self.user { 35 | write!(f, "{user}")?; 36 | } 37 | if let Some(grp) = &self.group { 38 | write!(f, ":{grp}")?; 39 | } 40 | Ok(()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/dregistry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dregistry" 3 | version = "0.1.0" 4 | description = "Docker registry client that supports downloading images from index.docker.io, ghcr.io, and others." 5 | 6 | edition.workspace = true 7 | authors.workspace = true 8 | license.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | categories.workspace = true 12 | keywords.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [dependencies] 18 | camino = { workspace = true } 19 | hex = { workspace = true } 20 | indicatif = { workspace = true } 21 | log = { workspace = true } 22 | logos = { workspace = true } 23 | pest = { workspace = true } 24 | pest_consume = { workspace = true } 25 | reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] } 26 | serde = { workspace = true, features = ["derive"] } 27 | serde-nested-json = { workspace = true } 28 | serde_json = { workspace = true } 29 | thiserror = { workspace = true } 30 | 31 | [dev-dependencies] 32 | clap = { workspace = true, features = ["derive"] } 33 | colog = { workspace = true } 34 | log = { workspace = true } 35 | maplit = { workspace = true } 36 | pretty_assertions = { workspace = true } 37 | -------------------------------------------------------------------------------- /src/template/log.rs: -------------------------------------------------------------------------------- 1 | use minijinja::value::Rest; 2 | use minijinja::{Environment, State}; 3 | 4 | #[allow(clippy::needless_pass_by_value)] 5 | pub fn trace(state: &State, args: Rest) { 6 | trace!("[{}]: {}", state.name(), args.join(" ")); 7 | } 8 | 9 | #[allow(clippy::needless_pass_by_value)] 10 | pub fn debug(state: &State, args: Rest) { 11 | debug!("[{}]: {}", state.name(), args.join(" ")); 12 | } 13 | 14 | #[allow(clippy::needless_pass_by_value)] 15 | pub fn info(state: &State, args: Rest) { 16 | info!("[{}]: {}", state.name(), args.join(" ")); 17 | } 18 | 19 | #[allow(clippy::needless_pass_by_value)] 20 | pub fn warning(state: &State, args: Rest) { 21 | warn!("[{}]: {}", state.name(), args.join(" ")); 22 | } 23 | 24 | #[allow(clippy::needless_pass_by_value)] 25 | pub fn error(state: &State, args: Rest) { 26 | error!("[{}]: {}", state.name(), args.join(" ")); 27 | } 28 | 29 | pub fn add_functions(env: &mut Environment) { 30 | env.add_function("trace", trace); 31 | env.add_function("debug", debug); 32 | env.add_function("info", info); 33 | env.add_function("warning", warning); 34 | env.add_function("error", error); 35 | } 36 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, OpenOptions}; 2 | use std::io::{BufWriter, Read, Write}; 3 | use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; 4 | 5 | use camino::Utf8Path; 6 | 7 | use crate::RaptorResult; 8 | 9 | const BUFFER_SIZE: usize = 128 * 1024; 10 | 11 | pub fn io_fast_copy(mut src: impl Read, dst: impl Write) -> RaptorResult<()> { 12 | let mut dst = BufWriter::with_capacity(BUFFER_SIZE, dst); 13 | std::io::copy(&mut src, &mut dst)?; 14 | Ok(()) 15 | } 16 | 17 | pub fn copy_file(from: impl AsRef, to: impl AsRef) -> RaptorResult<()> { 18 | let src = File::open(from.as_ref())?; 19 | let mode = src.metadata()?.permissions().mode(); 20 | let dst = OpenOptions::new() 21 | .write(true) 22 | .create(true) 23 | .truncate(true) 24 | .mode(mode) 25 | .open(to.as_ref())?; 26 | 27 | io_fast_copy(src, dst) 28 | } 29 | 30 | pub fn link_or_copy_file(from: impl AsRef, to: impl AsRef) -> RaptorResult<()> { 31 | std::fs::hard_link(from.as_ref(), to.as_ref()).or_else(|_| copy_file(from, to)) 32 | } 33 | 34 | pub mod capture_proc_fd; 35 | pub mod clapcolor; 36 | pub mod flag; 37 | pub mod kwargs; 38 | pub mod tty; 39 | -------------------------------------------------------------------------------- /src/template/file.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use camino::Utf8PathBuf; 4 | use minijinja::value::{Object, from_args}; 5 | use minijinja::{Environment, Error, ErrorKind, State, Value}; 6 | 7 | #[derive(Debug)] 8 | struct PathWrap(Utf8PathBuf); 9 | 10 | impl Object for PathWrap { 11 | fn call_method( 12 | self: &Arc, 13 | _state: &State, 14 | method: &str, 15 | args: &[Value], 16 | ) -> Result { 17 | () = from_args(args)?; 18 | match method { 19 | "exists" => Ok(Value::from(self.0.exists())), 20 | "is_dir" => Ok(Value::from(self.0.is_dir())), 21 | "is_file" => Ok(Value::from(self.0.is_file())), 22 | "is_symlink" => Ok(Value::from(self.0.is_symlink())), 23 | _ => Err(Error::from(ErrorKind::UnknownMethod)), 24 | } 25 | } 26 | } 27 | 28 | #[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] 29 | pub fn path(name: &str) -> Result { 30 | let obj = PathWrap(Utf8PathBuf::from(name)); 31 | let value = Value::from_object(obj); 32 | 33 | Ok(value) 34 | } 35 | 36 | pub fn add_functions(env: &mut Environment) { 37 | env.add_function("path", path); 38 | } 39 | -------------------------------------------------------------------------------- /src/tui/statusbar.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::{Color, Style, Stylize}; 2 | use ratatui::text::{Line, Span}; 3 | use ratatui::widgets::Widget; 4 | 5 | #[derive(Default)] 6 | pub struct StatusBar<'a> { 7 | spans: Vec>, 8 | } 9 | 10 | impl<'a> StatusBar<'a> { 11 | #[must_use] 12 | pub const fn new() -> Self { 13 | Self { spans: Vec::new() } 14 | } 15 | 16 | pub fn add>>(&mut self, span: S) { 17 | self.spans.push(span.into()); 18 | self.separator(); 19 | } 20 | 21 | pub fn counter>>(&mut self, number: usize, span: S) { 22 | self.spans 23 | .push(Span::raw(format!("{number:5} ")).bold().white()); 24 | self.spans.push(span.into()); 25 | 26 | self.separator(); 27 | } 28 | 29 | pub fn separator(&mut self) { 30 | self.spans.push(Span::raw(" | ").white().bold()); 31 | } 32 | } 33 | 34 | impl Widget for StatusBar<'_> { 35 | fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 36 | where 37 | Self: Sized, 38 | { 39 | let line = Line::from(self.spans).style(Style::new().bg(Color::Blue)); 40 | 41 | line.render(area, buf); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/sandbox/ext.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use camino::Utf8Path; 4 | 5 | use crate::RaptorResult; 6 | use crate::sandbox::FalconClient; 7 | use raptor_parser::ast::Chown; 8 | 9 | pub trait SandboxExt { 10 | fn shell(&mut self, cmd: &[&str]) -> RaptorResult<()>; 11 | fn write_file( 12 | &mut self, 13 | path: impl AsRef, 14 | owner: Option, 15 | mode: Option, 16 | data: impl AsRef<[u8]>, 17 | ) -> RaptorResult<()>; 18 | } 19 | 20 | impl SandboxExt for FalconClient { 21 | fn shell(&mut self, cmd: &[&str]) -> RaptorResult<()> { 22 | let mut args = vec!["/bin/sh", "-c"]; 23 | args.extend(cmd); 24 | self.run( 25 | &args 26 | .into_iter() 27 | .map(ToString::to_string) 28 | .collect::>(), 29 | ) 30 | } 31 | 32 | fn write_file( 33 | &mut self, 34 | path: impl AsRef, 35 | owner: Option, 36 | mode: Option, 37 | data: impl AsRef<[u8]>, 38 | ) -> RaptorResult<()> { 39 | let mut fd = self.create_file(path.as_ref(), owner, mode)?; 40 | fd.write_all(data.as_ref())?; 41 | drop(fd); 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /book/src/module-name/relative.md: -------------------------------------------------------------------------------- 1 | ## Relative module names 2 | 3 | The first, and arguably simplest form, is the *Relative* name. It is 4 | characterized by not having a dollar sign (`$`) in front of the first element, 5 | unlike the other two forms. 6 | 7 | Relative module paths form a sequence of one or more *elements*, which are the 8 | names between the dots (`.`). 9 | 10 | Each element before the last is viewed as a directory. The last element is the 11 | file name, appended with either `.rapt` for `FROM` instructions, or `.rinc` for 12 | `INCLUDE` instructions. 13 | 14 | In other words, `a.b.c.d` becomes `a/b/c/d.rapt` (`FROM`) or `a/b/c/d.rinc` (`INCLUDE`). 15 | 16 | ### Examples: 17 | 18 | | Statement | Source file | Resulting path | 19 | |:---------------------------|:------------------|:----------------------------| 20 | | `FROM base` | `target.rapt` | `base.rapt` | 21 | | `FROM base` | `lib/target.rapt` | `lib/base.rapt` | 22 | | `FROM utils.base` | `lib/target.rapt` | `lib/utils/base.rapt` | 23 | | `INCLUDE babelfish` | `lib/target.rapt` | `lib/babelfish.rinc` | 24 | | `INCLUDE hitchhiker.guide` | `lib/target.rapt` | `lib/hitchhiker/guide.rinc` | 25 | -------------------------------------------------------------------------------- /book/theme/ayu-highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | Based off of the Ayu theme 3 | Original by Dempfi (https://github.com/dempfi/ayu) 4 | */ 5 | 6 | .hljs { 7 | display: block; 8 | overflow-x: auto; 9 | background: #191f26; 10 | color: #e6e1cf; 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-quote { 15 | color: #8ca0a3; 16 | } 17 | 18 | .hljs-variable, 19 | .hljs-template-variable, 20 | .hljs-attribute, 21 | .hljs-attr, 22 | .hljs-regexp, 23 | .hljs-link, 24 | .hljs-selector-id, 25 | .hljs-selector-class { 26 | color: #ff7733; 27 | } 28 | 29 | .hljs-number, 30 | .hljs-meta, 31 | .hljs-builtin-name, 32 | .hljs-literal, 33 | .hljs-type, 34 | .hljs-params { 35 | color: #ffee99; 36 | } 37 | 38 | .hljs-string, 39 | .hljs-bullet { 40 | color: #b8cc52; 41 | } 42 | 43 | .hljs-title, 44 | .hljs-built_in, 45 | .hljs-section { 46 | color: #ffb454; 47 | } 48 | 49 | .hljs-keyword, 50 | .hljs-selector-tag, 51 | .hljs-symbol { 52 | color: #ff7733; 53 | } 54 | 55 | .hljs-name { 56 | color: #36a3d9; 57 | } 58 | 59 | .hljs-tag { 60 | color: #00568d; 61 | } 62 | 63 | .hljs-emphasis { 64 | font-style: italic; 65 | } 66 | 67 | .hljs-strong { 68 | font-weight: bold; 69 | } 70 | 71 | .hljs-addition { 72 | color: #91b362; 73 | } 74 | 75 | .hljs-deletion { 76 | color: #d96c75; 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/mdbook-deploy.yml: -------------------------------------------------------------------------------- 1 | name: mdbook-deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | pages: write 11 | id-token: write 12 | 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Install mdBook 21 | run: | 22 | export MDBOOK_URL=https://github.com/rust-lang/mdBook/releases/download/v0.4.52/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz 23 | export MDBOOK_ADMONISH_URL=https://github.com/tommilligan/mdbook-admonish/releases/download/v1.20.0/mdbook-admonish-v1.20.0-x86_64-unknown-linux-musl.tar.gz 24 | curl -L ${MDBOOK_URL} | sudo tar -C /usr/local/bin -xz 25 | curl -L ${MDBOOK_ADMONISH_URL} | sudo tar -C /usr/local/bin -xz 26 | 27 | - name: Install mdbook-pikchr from crates.io 28 | uses: baptiste0928/cargo-install@v3 29 | with: 30 | crate: mdbook-pikchr 31 | 32 | - name: Build the book 33 | run: mdbook build book 34 | 35 | - name: Deploy to GitHub Pages 36 | uses: peaceiris/actions-gh-pages@v4 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_dir: ./book/book 40 | -------------------------------------------------------------------------------- /crates/dregistry/src/reference.pest: -------------------------------------------------------------------------------- 1 | ASCII_LOWER_AND_DIGIT = _{ ASCII_ALPHA_LOWER | ASCII_DIGIT } 2 | 3 | DOCKER_REFERENCE = { SOI ~ container ~ EOI } 4 | 5 | container = { image_name ~ ( tag_spec | digest_spec )? } 6 | 7 | image_name = { prefix? ~ name_components } 8 | 9 | prefix = { hostname ~ (":" ~ port)? ~ "/" } 10 | hostname = { "localhost" | hostname_fqdn } 11 | hostname_fqdn = { hostname_part ~ ("." ~ hostname_part)+ } 12 | hostname_part = { (ASCII_ALPHA | ASCII_DIGIT | "-")+ } 13 | port = { ASCII_DIGIT+ } 14 | 15 | name_components = { name_component ~ ( "/" ~ name_component )* } 16 | name_component = _{ name ~ ( name_separator ~ name )* } 17 | name = _{ ('a'..'z' | '0'..'9')+ } 18 | name_separator = _{ ( "." | "_" ~ "_"? | "-"+ ) } 19 | 20 | tag_spec = _{ ":" ~ tag_name } 21 | tag_name = { tag_head ~ tag_tail{0,127} } 22 | tag_head = _{ ASCII_ALPHANUMERIC } 23 | tag_tail = _{ tag_head | "." | "-" } 24 | 25 | digest_spec = _{ "@" ~ digest } 26 | digest = { algorithm ~ ":" ~ encoded } 27 | algorithm = { algorithm_component ~ (algorithm_separator ~ algorithm_component)* } 28 | algorithm_component = { ASCII_LOWER_AND_DIGIT+ } 29 | algorithm_separator = { "+" | "." | "_" | "-" } 30 | encoded = { (ASCII_ALPHANUMERIC | "=" | "_" | "-")+ } 31 | -------------------------------------------------------------------------------- /crates/dregistry/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::reference::Rule; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum DockerError { 5 | #[error(transparent)] 6 | IoError(#[from] std::io::Error), 7 | 8 | #[error(transparent)] 9 | ReqwestError(#[from] reqwest::Error), 10 | 11 | #[error(transparent)] 12 | FromHexError(#[from] hex::FromHexError), 13 | 14 | #[error(transparent)] 15 | SerdeJsonError(#[from] serde_json::Error), 16 | 17 | #[error(transparent)] 18 | ParseIntError(#[from] std::num::ParseIntError), 19 | 20 | #[error(transparent)] 21 | ToStrError(#[from] reqwest::header::ToStrError), 22 | 23 | #[error(transparent)] 24 | PestError(#[from] Box>), 25 | 26 | #[error(transparent)] 27 | ParseError(#[from] crate::authparse::ParseError), 28 | 29 | #[error("Registry uses unsupported authentication method (only \"Bearer\" is supported)")] 30 | UnsupportedAuthMethod, 31 | 32 | #[error("Could not parse digest")] 33 | DigestError, 34 | 35 | #[error("Manifest not found for selected os/architecture")] 36 | ManifestNotFound, 37 | } 38 | 39 | pub type DResult = Result; 40 | 41 | impl From> for DockerError { 42 | fn from(e: pest_consume::Error) -> Self { 43 | Self::PestError(Box::new(e)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/mount.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use camino::Utf8PathBuf; 4 | use colored::Colorize; 5 | 6 | use crate::print::Theme; 7 | 8 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] 9 | pub enum MountType { 10 | File, 11 | Simple, 12 | Layers, 13 | Overlay, 14 | } 15 | 16 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 17 | pub struct MountOptions { 18 | pub mtype: MountType, 19 | pub readonly: bool, 20 | pub optional: bool, 21 | } 22 | 23 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 24 | pub struct InstMount { 25 | pub opts: MountOptions, 26 | pub name: String, 27 | pub dest: Utf8PathBuf, 28 | } 29 | 30 | impl Display for InstMount { 31 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 32 | f.keyword("MOUNT")?; 33 | write!(f, "{}", &self.opts)?; 34 | f.name(&self.name)?; 35 | f.dest(&self.dest) 36 | } 37 | } 38 | 39 | impl Display for MountOptions { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 | write!(f, " {}", self.mtype) 42 | } 43 | } 44 | 45 | impl Display for MountType { 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | let name = match self { 48 | Self::File => "--file", 49 | Self::Simple => "--simple", 50 | Self::Layers => "--layers", 51 | Self::Overlay => "--overlay", 52 | }; 53 | 54 | write!(f, "{}", name.bright_white()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /book/src/module-name/absolute.md: -------------------------------------------------------------------------------- 1 | ## Absolute module names 2 | 3 | Relative module names are simple, but lack the ability to point upwards in the 4 | directory heirarchy. 5 | 6 | For example, suppose you want to organize a set of Raptor files like so: 7 | 8 | ``` 9 | ~/project/base.rapt 10 | ~/project/hostname.rinc 11 | ~/project/target/common.rapt 12 | ~/project/target/frontend.rapt 13 | ~/project/target/database.rapt 14 | ~/project/lib/utils/tools.rinc 15 | ``` 16 | 17 | The `base` layer can `INCLUDE hostname`, but the `frontend` and `database` 18 | targets have no way to specify they want to build `FROM` the `base` layer! 19 | 20 | This is where *absolute* module names are useful. 21 | 22 | By prefixing a name with `$.` (dollar + dot) the name refers to the root of the 23 | current package. 24 | 25 | We will go into more detail of what a package is, in the next section. For now, 26 | it is enough to know that when invoking `raptor` on a target, the root for that 27 | target is the directory that `raptor` was started from. 28 | 29 | | Statement | Source file | Resulting path | 30 | |:----------------------------|:-----------------------|:-----------------------| 31 | | `FROM $.base` | `target/frontend.rapt` | `base.rapt` | 32 | | `FROM common` | `target/frontend.rapt` | `target/common.rapt` | 33 | | `FROM $.target.common` | `target/frontend.rapt` | `target/common.rapt` | 34 | | `INCLUDE $.lib.utils.tools` | `target/database.rapt` | `lib/utils/tools.rapt` | 35 | -------------------------------------------------------------------------------- /src/dsl/item.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::hash::{Hash, Hasher}; 3 | use std::sync::Arc; 4 | 5 | use camino::Utf8Path; 6 | use minijinja::Value; 7 | 8 | use crate::dsl::Program; 9 | use raptor_parser::ast::{Instruction, Origin, Statement}; 10 | 11 | #[derive(Clone, PartialEq, Eq)] 12 | pub enum Item { 13 | Statement(Statement), 14 | Program(Arc), 15 | } 16 | 17 | impl Item { 18 | #[must_use] 19 | pub fn program( 20 | code: impl IntoIterator, 21 | ctx: Value, 22 | path: impl AsRef, 23 | ) -> Self { 24 | Self::Program(Arc::new(Program { 25 | code: code.into_iter().collect(), 26 | ctx, 27 | path: path.as_ref().into(), 28 | })) 29 | } 30 | 31 | #[must_use] 32 | pub const fn statement(inst: Instruction, origin: Origin) -> Self { 33 | Self::Statement(Statement { inst, origin }) 34 | } 35 | } 36 | 37 | impl Debug for Item { 38 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 39 | match self { 40 | Self::Program(prog) => prog.fmt(f), 41 | Self::Statement(stmt) => stmt.fmt(f), 42 | } 43 | } 44 | } 45 | 46 | impl Hash for Item { 47 | fn hash(&self, state: &mut H) { 48 | match self { 49 | Self::Statement(statement) => statement.inst.hash(state), 50 | Self::Program(program) => { 51 | program.code.hash(state); 52 | program.ctx.hash(state); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/falcon/src/umask_proc.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::process::CommandExt; 2 | use std::process::Command; 3 | 4 | use nix::sys::stat::{Mode, umask}; 5 | 6 | /// Command extension to set umask for spawned child process 7 | pub trait Umask { 8 | fn umask(&mut self, umask: Mode) -> &mut Self; 9 | } 10 | 11 | impl Umask for Command { 12 | fn umask(&mut self, mode: Mode) -> &mut Self { 13 | unsafe { 14 | self.pre_exec(move || { 15 | umask(mode); 16 | Ok(()) 17 | }) 18 | } 19 | } 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use std::process::Command; 25 | 26 | use nix::sys::stat::Mode; 27 | 28 | use crate::error::FalconResult; 29 | use crate::umask_proc::Umask; 30 | 31 | fn test_umask(mask: u32) -> FalconResult<()> { 32 | Command::new("/bin/sh") 33 | .arg("-c") 34 | .umask(Mode::from_bits_truncate(mask)) 35 | .arg(format!("[ $(umask) = {mask:03o} ]")) 36 | .status()?; 37 | 38 | Ok(()) 39 | } 40 | 41 | #[test] 42 | fn set_umask_000() -> FalconResult<()> { 43 | test_umask(0o000) 44 | } 45 | 46 | #[test] 47 | fn set_umask_007() -> FalconResult<()> { 48 | test_umask(0o007) 49 | } 50 | 51 | #[test] 52 | fn set_umask_022() -> FalconResult<()> { 53 | test_umask(0o022) 54 | } 55 | 56 | #[test] 57 | fn set_umask_027() -> FalconResult<()> { 58 | test_umask(0o027) 59 | } 60 | 61 | #[test] 62 | fn set_umask_777() -> FalconResult<()> { 63 | test_umask(0o777) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /book/src/inst/include.md: -------------------------------------------------------------------------------- 1 | # Instruction `INCLUDE` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | INCLUDE [...=] 6 | ``` 7 | ~~~ 8 | 9 | ```admonish tip 10 | See the section on [module names](/module-name.md). 11 | ``` 12 | 13 | The `INCLUDE` instruction points to a Raptor include file (`.rinc`) to be 14 | executed. When using `INCLUDE`, any number of local variables can be passed 15 | to the included target. 16 | 17 | For example, if we have previously made the file `lib/install-utils.rinc` that 18 | installs some useful programs, we can use that file in build targets: 19 | 20 | ```raptor 21 | # Note: We use module name notation when including files 22 | # 23 | # The file is called `lib/install-utils.rinc`, which makes 24 | # the module name `lib.install-utils` 25 | INCLUDE lib.install-utils 26 | ``` 27 | 28 | We can also make the component accept parameters, to make powerful, flexible 29 | components: 30 | 31 | ```raptor 32 | # hypothetical library function to update "/etc/hostname" 33 | INCLUDE lib.set-hostname hostname="server1" 34 | ``` 35 | 36 | In the above example, we set the hostname of a server using an included 37 | component. 38 | 39 | ~~~admonish tip 40 | Since all values have to be specified as `key=value`, we might end up passing 41 | variables through several raptor files. This often ends up looking like this in 42 | the middle: 43 | ```raptor 44 | INCLUDE setup-thing username=username password=password 45 | ``` 46 | This is of course valid, but a shorter syntax exists for this case: 47 | ```raptor 48 | INCLUDE setup-thing username password 49 | ``` 50 | In other words, an include parameter `name=name` can always be shortened to `name`. 51 | ~~~ 52 | -------------------------------------------------------------------------------- /book/src/walkthrough/debian/index.md: -------------------------------------------------------------------------------- 1 | # Making a Debian liveboot iso with Raptor 2 | 3 | In this walkthrough, we will take a look at all the steps needed to make a 4 | customized Debian liveboot `.iso` image. 5 | 6 | By putting the image on a USB stick or external hard drive, any computer can be 7 | booted from this external device, without affecting existing data on the 8 | computer. 9 | 10 | Using this technique, you will be able to create custom images, containing the 11 | tools, programs and settings that you like, and build them consistently and 12 | easily with raptor. 13 | 14 | This guide is presented as a three-step process: 15 | 16 | - [Build](build.md): Build raptor layers suitable for live booting. 17 | 18 | - [Generate](iso.md): Generate a live boot iso from the layers 19 | 20 | - [Make](make.md): Use raptor-make to simplify the build process **\[optional\]** 21 | 22 | ~~~admonish tldr 23 | Below is an absolutely minimal example of generating a liveboot iso, for the impatient. 24 | ~~~ 25 | 26 | ---- 27 | 28 | Create a raptor file: 29 | 30 | ~~~admonish note title="base.rapt" 31 | ```raptor 32 | {{#include ../../../example/base.rapt}} 33 | ``` 34 | ~~~ 35 | 36 | Git clone (or otherwise download) the necessary build container recipes: 37 | ```sh 38 | git clone https://github.com/chrivers/raptor-builders.git 39 | ``` 40 | 41 | Then use these to build `liveboot.iso`: 42 | 43 | ```sh 44 | sudo raptor run -L rbuild raptor-builders '$rbuild.deblive' -I base -O liveboot.iso 45 | ``` 46 | 47 | ---- 48 | 49 | After this command has run, you should have the file `liveboot.iso` available. Enjoy! 50 | 51 | To learn how (and why) this works, **read the next chapters of this guide.** 52 | -------------------------------------------------------------------------------- /src/build/present.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use itertools::Itertools; 3 | 4 | use crate::RaptorResult; 5 | use crate::build::{BuildTarget, BuildTargetStats}; 6 | use crate::dsl::Item; 7 | 8 | pub struct Presenter<'a>(&'a BuildTargetStats); 9 | 10 | impl<'a> Presenter<'a> { 11 | #[must_use] 12 | pub const fn new(stats: &'a BuildTargetStats) -> Self { 13 | Self(stats) 14 | } 15 | 16 | pub fn present_program(&self, name: &str, indent: usize) -> RaptorResult<()> { 17 | let prefix = "| ".repeat(indent); 18 | println!("{prefix}"); 19 | println!("{prefix}{} [{}]", "# target".dimmed(), name.bright_white()); 20 | 21 | if let BuildTarget::Program(program) = &self.0.targets[name] { 22 | for inst in &program.code { 23 | match inst { 24 | Item::Statement(stmt) => { 25 | println!("{prefix}{}", stmt.inst); 26 | } 27 | 28 | Item::Program(_program) => { 29 | println!("{prefix}"); 30 | } 31 | } 32 | } 33 | } 34 | 35 | if let Some(rmap) = self.0.rmap.get(name) { 36 | for sub in rmap.iter().sorted() { 37 | self.present_program(sub, indent + 1)?; 38 | } 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | pub fn present(&self) -> RaptorResult<()> { 45 | for root in self.0.roots.iter().sorted() { 46 | info!("{:-<80}", format!("--[ {} ]", root.bright_yellow())); 47 | self.present_program(root, 0)?; 48 | println!(); 49 | } 50 | 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/falcon/src/client/protocol.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub enum Account { 6 | Id(u32), 7 | Name(String), 8 | } 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub struct RequestRun { 12 | pub arg0: String, 13 | pub argv: Vec, 14 | } 15 | 16 | #[derive(Debug, Serialize, Deserialize)] 17 | pub struct RequestCreateFile { 18 | pub path: Utf8PathBuf, 19 | pub user: Option, 20 | pub group: Option, 21 | pub mode: Option, 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize)] 25 | pub struct RequestCreateDir { 26 | pub path: Utf8PathBuf, 27 | pub user: Option, 28 | pub group: Option, 29 | pub mode: Option, 30 | pub parents: bool, 31 | } 32 | 33 | #[derive(Debug, Serialize, Deserialize)] 34 | pub struct RequestWriteFd { 35 | pub fd: i32, 36 | pub data: Vec, 37 | } 38 | 39 | #[derive(Debug, Serialize, Deserialize)] 40 | pub struct RequestCloseFd { 41 | pub fd: i32, 42 | } 43 | 44 | #[derive(Debug, Serialize, Deserialize)] 45 | pub struct RequestChangeDir { 46 | pub cd: String, 47 | } 48 | 49 | #[derive(Debug, Serialize, Deserialize)] 50 | pub struct RequestSetEnv { 51 | pub key: String, 52 | pub value: String, 53 | } 54 | 55 | #[derive(Debug, Serialize, Deserialize)] 56 | pub enum Request { 57 | Run(RequestRun), 58 | ChangeDir(RequestChangeDir), 59 | SetEnv(RequestSetEnv), 60 | CreateFile(RequestCreateFile), 61 | CreateDir(RequestCreateDir), 62 | WriteFd(RequestWriteFd), 63 | CloseFd(RequestCloseFd), 64 | Shutdown, 65 | } 66 | 67 | pub type Response = Result; 68 | -------------------------------------------------------------------------------- /tests/falcon.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::net::UnixListener; 2 | use std::process::Command; 3 | 4 | use camino_tempfile::{NamedUtf8TempFile, Utf8TempDir}; 5 | use nix::errno::Errno; 6 | 7 | use raptor::sandbox::{FalconClient, SandboxExt}; 8 | use raptor::{RaptorError, RaptorResult}; 9 | 10 | const TEST_DATA: &[u8] = b"Raptortest\n"; 11 | 12 | fn spawn_client() -> RaptorResult { 13 | let exe = std::env::current_exe().unwrap(); 14 | let deps = exe.parent().unwrap().parent().unwrap(); 15 | let client = deps.join("falcon"); 16 | 17 | let tempdir = Utf8TempDir::new()?; 18 | let socket_path = tempdir.path().join("raptor-test"); 19 | let listen = UnixListener::bind(&socket_path)?; 20 | 21 | let proc = Command::new(client) 22 | .env("FALCON_LOG_LEVEL", "off") 23 | .env("FALCON_SOCKET", socket_path.as_str()) 24 | .spawn()?; 25 | 26 | let conn = listen.accept()?.0; 27 | 28 | Ok(FalconClient::new(proc, conn)) 29 | } 30 | 31 | #[test] 32 | fn chdir_tmp() -> RaptorResult<()> { 33 | let mut sc = spawn_client()?; 34 | sc.chdir("/tmp")?; 35 | sc.close() 36 | } 37 | 38 | #[test] 39 | fn write_file_over_dir() -> RaptorResult<()> { 40 | let mut sc = spawn_client()?; 41 | match sc.create_file("/tmp".into(), None, None).unwrap_err() { 42 | RaptorError::SandboxRequestError(Errno::EISDIR) => {} 43 | err => panic!("unexpected error: {err}"), 44 | } 45 | sc.close() 46 | } 47 | 48 | #[test] 49 | fn client_write_file() -> RaptorResult<()> { 50 | let mut sc = spawn_client()?; 51 | 52 | let tmpfile = NamedUtf8TempFile::new()?; 53 | let path = tmpfile.path(); 54 | 55 | sc.write_file(path, None, None, TEST_DATA)?; 56 | 57 | assert_eq!(std::fs::read(path)?, TEST_DATA); 58 | 59 | sc.close() 60 | } 61 | -------------------------------------------------------------------------------- /book/src/module-name.md: -------------------------------------------------------------------------------- 1 | # Module names 2 | 3 | In Raptor, "module names" are the names Raptor files use to refer to other 4 | Raptor files. 5 | 6 | This is the case both for the `FROM` instruction (where the modules have the 7 | `.rapt` extension) and `INCLUDE` (`.rinc` extension). 8 | 9 | There are three types of module names; Relative, Absolute and Package names. 10 | 11 | | Type | Example | Description | 12 | |:---------|:------------|-------------------------------------------------------------| 13 | | Relative | `foo.bar` | Used to refer to paths at or below the current directory | 14 | | Absolute | `$.foo.bar` | Used to refer to paths from the root of the current package | 15 | | Package | `$foo.bar` | Used to refer to paths in other packages | 16 | 17 | The difference between these forms is how they resolve to paths in the 18 | filesystem. For a detailed explanation of each, see the following sections. 19 | 20 | ## Instancing 21 | 22 | Each of these forms support *instancing*, which is a way to pass a single, 23 | simple parameter through the module name, by appending a `@` followed by the 24 | value, to the module name. 25 | 26 | All raptor files are either instanced (`example@.rapt` / `example@.rinc`) or not 27 | (`example.rapt` / `example.rinc`). 28 | 29 | Non-instanced files *cannot* be referenced with an instanced name, and vice 30 | versa. 31 | 32 | Users of `systemd` might recognize this pattern, which is used in the same way 33 | there. 34 | 35 | To learn more, read the [section on instancing](instancing.md). 36 | 37 | ## Learn more 38 | 39 | Each type of module name is described in more detail: 40 | 41 | - [Relative](module-name/relative.md) 42 | - [Absolute](module-name/absolute.md) 43 | - [Package](module-name/package.md) 44 | -------------------------------------------------------------------------------- /src/program/resolve.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use minijinja::Value; 4 | 5 | use raptor_parser::ast::{Expression, IncludeArg}; 6 | 7 | use crate::{RaptorError, RaptorResult}; 8 | 9 | pub trait ResolveArg { 10 | fn resolve(&self, arg: Expression) -> RaptorResult; 11 | } 12 | 13 | pub trait ResolveArgs { 14 | fn resolve_args<'a>(&'a self, args: &'a [IncludeArg]) -> RaptorResult>; 15 | } 16 | 17 | impl ResolveArg for Value { 18 | fn resolve(&self, arg: Expression) -> RaptorResult { 19 | match arg { 20 | Expression::Lookup(lookup) => { 21 | let mut val = self.get_attr(&lookup.path.parts()[0])?; 22 | if val.is_undefined() { 23 | return Err(RaptorError::UndefinedVarError( 24 | lookup.path.parts()[0].clone(), 25 | lookup.origin.clone(), 26 | )); 27 | } 28 | 29 | for name in &lookup.path.parts()[1..] { 30 | val = val.get_attr(name)?; 31 | if val.is_undefined() { 32 | return Err(RaptorError::UndefinedVarError( 33 | name.into(), 34 | lookup.origin.clone(), 35 | )); 36 | } 37 | } 38 | 39 | Ok(val) 40 | } 41 | 42 | Expression::Value(val) => Ok(val), 43 | } 44 | } 45 | } 46 | 47 | impl ResolveArgs for Value { 48 | fn resolve_args<'a>(&'a self, args: &'a [IncludeArg]) -> RaptorResult> { 49 | args.iter() 50 | .map(|IncludeArg { name, value }| Ok((name.as_str(), self.resolve(value.clone())?))) 51 | .collect() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /book/src/instancing.md: -------------------------------------------------------------------------------- 1 | # Instancing 2 | 3 | Raptor files can be *instanced*, which makes them work as a template. 4 | 5 | Instanced files can be recognized by ending in `@.rapt` (for `FROM`) or `@.rinc` 6 | (for `INCLUDE`). 7 | 8 | ## File names and syntax 9 | 10 | | File | Instanced? | Example | 11 | |------------------|------------|--------------------------| 12 | | `base.rapt` | No | `FROM base` | 13 | | `server@.rapt` | Yes | `FROM server@production` | 14 | | `settings.rinc` | No | `INCLUDE settings` | 15 | | `firewall@.rinc` | Yes | `INCLUDE firewall@full` | 16 | 17 | The table above shows some examples of instanced and non-instanced Raptor files. 18 | 19 | ~~~admonish error title="Beware" 20 | It is **invalid** to reference an instanced file without an instance. 21 | 22 | For example, `FROM server@` or `FROM base@value` would both fail to compile. 23 | 24 | Therefore, when writing a new Raptor file, you need to determine if it needs to 25 | be instanced, and name the file accordingly. 26 | ~~~ 27 | 28 | ## Using instancing 29 | 30 | So far, we have seen how to create templated (instanced) Raptor files, and how 31 | to reference them to provide a value. 32 | 33 | Now we will see how to *use* the provided value, so that instancing becomes 34 | useful. Let us start the simplest possible example 35 | 36 | ~~~admonish note title="build-stamp@.rinc" 37 | ```raptor 38 | WRITE "We are instance {{instance}}\n" /root/build-stamp.txt 39 | ``` 40 | ~~~ 41 | 42 | This writes a human-readable line of text to a file, including the instance id. 43 | 44 | Now we can use it from another file: 45 | 46 | ~~~admonish note title="server.rapt" 47 | ```raptor 48 | # Will write "We are instance 1\n" to /root/build-stamp.txt 49 | INCLUDE build-stamp@1 50 | ``` 51 | ~~~ 52 | -------------------------------------------------------------------------------- /src/sandbox/file.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind, Write}; 2 | 3 | use camino::Utf8Path; 4 | 5 | use crate::sandbox::FalconClient; 6 | use crate::{RaptorError, RaptorResult}; 7 | use falcon::client::{Account, Request, RequestCloseFd, RequestCreateFile, RequestWriteFd}; 8 | use raptor_parser::ast::Chown; 9 | 10 | #[derive(Debug)] 11 | pub struct SandboxFile<'sb> { 12 | sandbox_client: &'sb mut FalconClient, 13 | fd: i32, 14 | } 15 | 16 | impl<'sb> SandboxFile<'sb> { 17 | pub fn new( 18 | sandbox_client: &'sb mut FalconClient, 19 | path: &Utf8Path, 20 | owner: Option, 21 | mode: Option, 22 | ) -> RaptorResult { 23 | let Chown { user, group } = owner.unwrap_or_default(); 24 | let fd = sandbox_client.rpc(&Request::CreateFile(RequestCreateFile { 25 | path: path.to_owned(), 26 | user: user.map(Account::Name), 27 | group: group.map(Account::Name), 28 | mode, 29 | }))?; 30 | Ok(Self { sandbox_client, fd }) 31 | } 32 | } 33 | 34 | impl Write for SandboxFile<'_> { 35 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 36 | match self.sandbox_client.rpc(&Request::WriteFd(RequestWriteFd { 37 | fd: self.fd, 38 | data: buf.to_vec(), 39 | })) { 40 | Ok(_) => Ok(buf.len()), 41 | Err(RaptorError::IoError(err)) => Err(err), 42 | Err(err) => Err(Error::new(ErrorKind::BrokenPipe, err)), 43 | } 44 | } 45 | 46 | fn flush(&mut self) -> std::io::Result<()> { 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl Drop for SandboxFile<'_> { 52 | fn drop(&mut self) { 53 | let _ = self 54 | .sandbox_client 55 | .rpc(&Request::CloseFd(RequestCloseFd { fd: self.fd })); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | - [Introduction](intro.md) 3 | - [Installling Raptor](install.md) 4 | 5 | --- 6 | 7 | # Walkthrough examples 8 | - [Debian Liveboot](walkthrough/debian/index.md) 9 | - [Build filesystem](walkthrough/debian/build.md) 10 | - [Generate iso](walkthrough/debian/iso.md) 11 | - [Use raptor-make](walkthrough/debian/make.md) 12 | 13 | --- 14 | 15 | # Raptor Builders 16 | 17 | - [Overview](builders/index.md) 18 | - [deblive](builders/deblive.md) 19 | - [live-disk-image](builders/live-disk-image.md) 20 | - [disk-image](builders/disk-image.md) 21 | - [part-image](builders/part-image.md) 22 | - [docker-image](builders/docker-image.md) 23 | - [Environment settings](builders/environment.md) 24 | 25 | --- 26 | 27 | # Learning Raptor 28 | 29 | - [Module names](module-name.md) 30 | - [Relative](module-name/relative.md) 31 | - [Absolute](module-name/absolute.md) 32 | - [Package](module-name/package.md) 33 | - [Instancing](instancing.md) 34 | - [String escape](string-escape.md) 35 | - [Expressions](expressions.md) 36 | - [File options](file-options.md) 37 | - [Mount types](mount-types.md) 38 | - [`--simple`](mount-types/simple.md) 39 | - [`--file`](mount-types/file.md) 40 | - [`--layers`](mount-types/layers.md) 41 | - [`--overlay`](mount-types/overlay.md) 42 | 43 | --- 44 | 45 | # Reference manual 46 | 47 | - [Raptor Make](make.md) 48 | - [Grammar](grammar.md) 49 | - [Instructions](syntax.md) 50 | - [Build instructions]() 51 | - [FROM](inst/from.md) 52 | - [RUN](inst/run.md) 53 | - [ENV](inst/env.md) 54 | - [WORKDIR](inst/workdir.md) 55 | 56 | - [WRITE](inst/write.md) 57 | - [MKDIR](inst/mkdir.md) 58 | - [COPY](inst/copy.md) 59 | 60 | - [INCLUDE](inst/include.md) 61 | - [RENDER](inst/render.md) 62 | - [Run instructions]() 63 | - [MOUNT](inst/mount.md) 64 | - [ENTRYPOINT](inst/entrypoint.md) 65 | - [CMD](inst/cmd.md) 66 | -------------------------------------------------------------------------------- /book/src/mount-types/overlay.md: -------------------------------------------------------------------------------- 1 | ## Mount type `--overlay` 2 | 3 | Like `--layers`, this is a Raptor-specific mount type. 4 | 5 | Raptor targets are almost always built from a set of layers (i.e., there's at 6 | least one `FROM` instruction). 7 | 8 | When running a raptor container, this stack of layers is combined using 9 | `overlayfs`, which makes the kernel present them to the container as a single, 10 | unified filesystem. 11 | 12 | For build containers that need to operate on this combined view of a build 13 | target, the `--overlay` mount type is available. 14 | 15 | For example, the [`disk-image` builder](../builders/disk-image.md) uses this mount type, in order to 16 | build disk images for virtual (or physical) machines. 17 | 18 | ~~~admonish note title="overlay-lister.rapt" 19 | ```raptor 20 | {{#include ../../example/overlay-lister.rapt}} 21 | ``` 22 | ~~~ 23 | 24 | ```sh 25 | $ sudo raptor run overlay-lister -I file-lister 26 | total 60 27 | lrwxrwxrwx 1 root root 7 Aug 24 16:20 bin -> usr/bin 28 | drwxr-xr-x 2 root root 4096 Aug 24 16:20 boot 29 | drwxr-xr-x 2 root root 4096 Sep 29 00:00 dev 30 | drwxr-xr-x 27 root root 4096 Sep 29 00:00 etc 31 | drwxr-xr-x 2 root root 4096 Aug 24 16:20 home 32 | lrwxrwxrwx 1 root root 7 Aug 24 16:20 lib -> usr/lib 33 | lrwxrwxrwx 1 root root 9 Aug 24 16:20 lib64 -> usr/lib64 34 | drwxr-xr-x 2 root root 4096 Sep 29 00:00 media 35 | drwxr-xr-x 2 root root 4096 Sep 29 00:00 mnt 36 | drwxr-xr-x 2 root root 4096 Sep 29 00:00 opt 37 | drwxr-xr-x 2 root root 4096 Sep 29 00:00 proc 38 | drwx------ 2 root root 4096 Sep 29 00:00 root 39 | drwxr-xr-x 3 root root 4096 Sep 29 00:00 run 40 | lrwxrwxrwx 1 root root 8 Aug 24 16:20 sbin -> usr/sbin 41 | drwxr-xr-x 2 root root 4096 Sep 29 00:00 srv 42 | drwxr-xr-x 2 root root 4096 Aug 24 16:20 sys 43 | drwxrwxrwt 2 root root 4096 Sep 29 00:00 tmp 44 | drwxr-xr-x 12 root root 4096 Sep 29 00:00 usr 45 | drwxr-xr-x 11 root root 4096 Sep 29 00:00 var 46 | ``` 47 | -------------------------------------------------------------------------------- /src/tui/logo.rs: -------------------------------------------------------------------------------- 1 | use ratatui::layout::{Constraint, Flex, Layout, Rect}; 2 | use ratatui::style::Stylize; 3 | use ratatui::widgets::Widget; 4 | use tui_big_text::{BigText, PixelSize}; 5 | 6 | pub struct RaptorLogo<'a> { 7 | text: BigText<'a>, 8 | } 9 | 10 | impl RaptorLogo<'_> { 11 | #[must_use] 12 | pub fn complete() -> Self { 13 | let text = BigText::builder() 14 | .pixel_size(PixelSize::Quadrant) 15 | .lines(vec![ 16 | "Raptor complete".light_blue().bold().into(), 17 | "---------------".dark_gray().into(), 18 | "press q to quit".white().into(), 19 | ]) 20 | .centered() 21 | .build(); 22 | 23 | Self { text } 24 | } 25 | 26 | #[must_use] 27 | pub fn failed() -> Self { 28 | let text = BigText::builder() 29 | .pixel_size(PixelSize::Quadrant) 30 | .lines(vec![ 31 | "".light_red().bold().into(), 32 | "---------------".dark_gray().into(), 33 | "press q to quit".white().into(), 34 | ]) 35 | .centered() 36 | .build(); 37 | 38 | Self { text } 39 | } 40 | } 41 | 42 | impl Widget for RaptorLogo<'_> { 43 | #[allow(clippy::cast_possible_truncation)] 44 | fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) 45 | where 46 | Self: Sized, 47 | { 48 | let rect = center( 49 | area, 50 | Constraint::Fill(1), 51 | Constraint::Length(self.text.lines.len() as u16 * 4), 52 | ); 53 | 54 | self.text.render(rect, buf); 55 | } 56 | } 57 | 58 | fn center(area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect { 59 | let [area] = Layout::horizontal([horizontal]) 60 | .flex(Flex::Center) 61 | .areas(area); 62 | let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area); 63 | area 64 | } 65 | -------------------------------------------------------------------------------- /book/src/module-name/package.md: -------------------------------------------------------------------------------- 1 | ## Package module names 2 | 3 | In the previous section, we briefly mentioned *packages*. 4 | 5 | Raptor is designed to work collaboratively, encouraging sharing of code between 6 | projects, and people. A Raptor package is simply a collection of useful Raptor 7 | files, typically distributed as a git repository. 8 | 9 | This means we need a robust way to refer to Raptor files outside of our current 10 | project, as well as a way to tell Raptor how to find these files. 11 | 12 | This is where *package* module names are used. 13 | 14 | First, let us take a look at how to make Raptor aware of external code 15 | bases. This is called *linking*, analogous to how the term is used when building 16 | a program from several sources. 17 | 18 | When invoking `raptor`, the `-L` option defines a linked raptor package: 19 | 20 | ```sh 21 | sudo raptor -L name src ... 22 | ``` 23 | 24 | For example, imagine we are working on a web service, and we want to generate 25 | the deployment with Raptor. Now imagine this requires a database server, and 26 | that someone else is responsible for the Raptor code that controls the database. 27 | 28 | Then we might have a file layout like so: 29 | 30 | ``` 31 | ~/project/web/server.rapt 32 | ~/project/database/db-setup.rinc 33 | ``` 34 | 35 | We fully control `~/projects/web`, but `~/project/database` is a git repository 36 | we only pull changes from. 37 | 38 | We would like to `INCLUDE` the `db-setup` module, but it exists outside our own 39 | repository. 40 | 41 | This can be solved by declaring `database` as linked package: 42 | 43 | ```sh 44 | $ cd ~/project/web 45 | $ sudo raptor build server -L database ../database 46 | ``` 47 | 48 | Now, we can refer to the content of `../database` by using `$database`. 49 | 50 | ~~~admonish tip 51 | We don't have to give the link the same name as the directory it refers to! 52 | 53 | If we link with `-L lib1 ../database`, we could instead refer to it as `$lib1.db-setup`. 54 | 55 | Feel free to use any link name that suits the project; the linked names have no impact outside your project. 56 | ~~~ 57 | -------------------------------------------------------------------------------- /book/src/make.md: -------------------------------------------------------------------------------- 1 | # Raptor make 2 | 3 | ## Overall structure 4 | 5 | Below is an example of the overall structure of a `Raptor.toml` file. 6 | 7 | **Note**: All parts are optional, so you only need to define the parts you need. 8 | 9 | ```toml 10 | [raptor.link] 11 | name1 = "path/to/source1" 12 | name2 = "path/to/source2" 13 | ... 14 | 15 | [run.purple] 16 | # ..run target here.. 17 | 18 | [run.orange] 19 | # ..run target here.. 20 | 21 | [group.green] 22 | # ..group here.. 23 | 24 | [group.yellow] 25 | # ..group here.. 26 | ``` 27 | 28 | ## Run target format 29 | 30 | A run target (`[run.]`) is the most commonly used feature in 31 | `Raptor.toml`. 32 | 33 | The structure for a job named `example` is shown below, where each field is 34 | specified with its default value. 35 | 36 | **Note**: Only the `target` field is required! Everything else can be specified as needed. 37 | 38 | ~~~admonish note title="Raptor.toml run" 39 | ```toml 40 | [run.example] 41 | target = 42 | 43 | # Cache mounts 44 | # (default is empty list) 45 | # 46 | # Note: A single element can be specified as a string instead of the list 47 | cache = [] 48 | 49 | # Input mounts 50 | # (default is empty list) 51 | # 52 | # Note: A single element can be specified as a string instead of the list 53 | input = [] 54 | 55 | # Output mounts 56 | # (default is empty list) 57 | # 58 | # Note: A single element can be specified as a string instead of the list 59 | output = [] 60 | 61 | # Entrypoint arguments 62 | entrypoint = [] 63 | 64 | # Command arguments 65 | args = [] 66 | 67 | # BTreeMap 68 | env = {} 69 | 70 | # State directory for container state 71 | # (default is unset, meaning ephemeral containers) 72 | #state_dir = 73 | ``` 74 | ~~~ 75 | 76 | ## Group format 77 | 78 | A group is used to collectively refer to a number of run and build jobs, by a 79 | single name. 80 | 81 | ```toml 82 | [group.example] 83 | # List of layers to build 84 | # 85 | # Default is empty list 86 | build = [] 87 | 88 | # List of names for [run.] targets to run 89 | # 90 | # Default is empty list 91 | run = [] 92 | ``` 93 | -------------------------------------------------------------------------------- /book/src/inst/from.md: -------------------------------------------------------------------------------- 1 | # Instruction: `FROM` 2 | 3 | ~~~admonish summary 4 | ```nginx 5 | FROM [://] 6 | ``` 7 | ~~~ 8 | 9 | The `FROM` instruction bases the current layer on top of some the specified layer. 10 | 11 | This instruction is *not* required. If no `FROM` instruction is used, the target 12 | is building from an empty base, with no dependencies. 13 | 14 | Multiple `FROM` instructions are **not supported**. 15 | 16 | ## From sources 17 | 18 | Raptor supports multiple options for `from-source`: 19 | 20 | | Type | Schema | Example | 21 | |:-------|:------------|:------------------------------| 22 | | Raptor | `` | `FROM library.base` | 23 | | Docker | `docker://` | `FROM docker://debian:trixie` | 24 | 25 | ### Raptor sources 26 | 27 | When no schema is specified, the `from-source` is assumed to be the [module 28 | name](../module-name.md) of another raptor layer. 29 | 30 | ~~~admonish tip 31 | This will be familiar to docker users. For example.. 32 | ```docker 33 | # Dockerfile 34 | FROM base 35 | ``` 36 | ..will depend on the docker image `base` 37 | ~~~ 38 | 39 | However, unlike docker files, raptor can point to raptor files in other 40 | directories, or even other packages. See [module names](../module-name.md) for 41 | an overview. 42 | 43 | #### Examples 44 | 45 | ```raptor 46 | # This will depend on `base.rapt` 47 | FROM base 48 | ``` 49 | 50 | ```raptor 51 | # This will depend on `library/debian.rapt` 52 | FROM library.debian 53 | ``` 54 | 55 | ### Docker sources 56 | 57 | To use a docker image as the basis for a raptor layer, specify the name of the 58 | docker image, prefixed with `docker://`, e.g: 59 | 60 | ```raptor 61 | FROM docker://debian:trixie 62 | ``` 63 | 64 | ~~~admonish tip 65 | In general, `docker pull ` becomes `FROM docker://` 66 | ~~~ 67 | 68 | There are multiple (optional) parts in a *docker reference*, which has a 69 | surprisingly intricate syntax. 70 | 71 | Raptor supports the entire grammar for docker references, so anything that 72 | `docker pull` will accept, should work with `FROM docker://` in raptor. 73 | -------------------------------------------------------------------------------- /book/src/string-escape.md: -------------------------------------------------------------------------------- 1 | # Strings 2 | 3 | In raptor files, string values can be expressed as quoted strings, or in certain 4 | cases as so-called *barewords*: 5 | 6 | ```raptor 7 | RUN echo these are barewords 8 | 9 | RUN "echo" "these" "are" "quoted" "string" 10 | ``` 11 | 12 | For example, the following two statements are equivalent: 13 | 14 | ```raptor 15 | RUN echo word1 word2 16 | 17 | RUN echo "word1" "word2" 18 | ``` 19 | 20 | But the following two statements are *not*: 21 | 22 | ```raptor 23 | # This creates 1 file called "filename with spaces" 24 | RUN touch "filename with spaces" 25 | 26 | # This creates *3 files* called "filename", "with", and "spaces", respectively 27 | RUN touch filename with spaces 28 | ``` 29 | 30 | ```admonish tip 31 | Think of barewords as a convenience, to avoid needing to quote everything all 32 | the time. 33 | 34 | It is **always** valid and safe to use quoted strings to clearly convey the 35 | intended meaning. 36 | 37 | When in doubt, use quotes. 38 | ``` 39 | 40 | # String escaping 41 | 42 | When using a quoted string, the backslash character (`\`) gains special meaning, 43 | and is known as the *escape character*. 44 | 45 | When it is followed by certain other characters, the combined expression is 46 | replaced in the string: 47 | 48 | | Escape expression | Result | 49 | |:------------------|:---------------------------------------------------------------------| 50 | | `\\` | A single literal backslash | 51 | | `\n` | Newline (as if the string continued on the next line of text) | 52 | | `\t` | Tabulator (useful to make tab clearly visible, and copy-paste proof) | 53 | | `\"` | Quote character (as opposed to ending the string) | 54 | 55 | A backslash followed by any other character will result in a parse error. 56 | 57 | ```admonish warning title="Important" 58 | Because backslash (`\`) is used as the escape character in quoted strings, any 59 | literal backslashes *must* themselves be escaped, by adding another backslash 60 | (`\\`). 61 | ``` 62 | -------------------------------------------------------------------------------- /book/src/walkthrough/debian/build.md: -------------------------------------------------------------------------------- 1 | # Building a bootable target 2 | 3 | In order to build a raptor target that can be turned into a bootable iso, there 4 | are a few requirements we need to consider: 5 | 6 | - The distribution must be Debian-based (or, preferably, Debian) 7 | - The package `live-boot` must be installed, since it contains the necessary 8 | tools and scripts 9 | - A linux kernel package must be installed (e.g. `linux-image-amd64`). 10 | 11 | Here is an example of a fairly minimal base layer, which can be turned into an iso: 12 | 13 | ~~~admonish title="base.rapt" 14 | ```raptor 15 | {{#include ../../../example/base.rapt}} 16 | ``` 17 | ~~~ 18 | 19 | The kernel package can take a bit of time to install, so let's start a new layer 20 | for further customization. This way, we don't need to rebuild the base layer 21 | with every change we make to the upper layer: 22 | 23 | ~~~admonish title="ssh.rapt" 24 | ```raptor 25 | {{#include ../../../example/ssh.rapt}} 26 | ``` 27 | ~~~ 28 | 29 | Of course, these layers could easily be combined, but it is good to get in a 30 | habit of separating things into reasonable layers. This improves caching and 31 | makes builds faster, since more can be reused between builds. 32 | 33 | Now we are ready to build the layers. Since `ssh` derives from `base` (which 34 | derives from a docker layer), we just have to request raptor to build 35 | `ssh`. Raptor automatically determines dependencies, and builds them as needed. 36 | 37 | ```sh 38 | sudo raptor build ssh 39 | ``` 40 | 41 | You should now see raptor build the `ssh` layer, with all command output from 42 | the `apt-get` commands being shown in the terminal. 43 | 44 | Once complete, you can quickly verify that the layer is complete, by running the 45 | same command again. This time it should very quickly display a message, 46 | indicating that each layer is complete: 47 | 48 | ```sh 49 | $ sudo raptor build ssh 50 | [*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie 51 | [*] Completed [AB1DD71BFD07718B] base 52 | [*] Completed [80E4F4E5B0E2F6BA] ssh 53 | ``` 54 | 55 | In the [next chapter](iso.md) we will see how we can turn these layers into a 56 | bootable iso file. 57 | -------------------------------------------------------------------------------- /book/src/expressions.md: -------------------------------------------------------------------------------- 1 | # Expressions 2 | 3 | Raptor supports a limited form of inline literal expressions (i.e. "values"), 4 | that can be specified when using the `INCLUDE` and `RENDER` instructions. 5 | 6 | ## Booleans 7 | 8 | Boolean values work identically to json (and many other languages): 9 | 10 | | Literal | Value | 11 | |:--------|:------| 12 | | `true` | true | 13 | | `false` | false | 14 | 15 | ## Integers 16 | 17 | Integer values are supported. 18 | 19 | Be aware that raptor always uses `i64` (signed 64-bit) integers. 20 | 21 | This is *unlike* the typical json implementations, that uses `f64` (64-bit 22 | floating-point) numbers, in two important ways: 23 | 24 | **A)** The valid range for i64 integers is `-9223372036854775808` to 25 | `9223372036854775807`, inclusive. This should be sufficient for most 26 | applications. Any integer in this range will be represented exactly. 27 | 28 | **B)** Floating-point (i.e. fractional) numbers are not supported. For example, 29 | `3.14` is *not* valid in raptor. Instead, consider passing such a value as a 30 | string. 31 | 32 | Currently, alternate integer bases (i.e. hexadecimal or octal) are *not* supported. 33 | 34 | ## Strings 35 | 36 | String are supported, and work much like they do in json, or other common notations. 37 | 38 | ```admonish tip 39 | See the section on [string escapes](/string-escape.md) for more details. 40 | ``` 41 | 42 | ## Lists 43 | 44 | Lists are supported, with a syntax very similar to json. The only difference is 45 | that raptor allows an optional trailing comma after the last list element, while 46 | json does not. 47 | 48 | ~~~admonish example title="Examples of lists" 49 | ```json 50 | [1, 2, 3] 51 | ``` 52 | 53 | ```json 54 | [true, false] 55 | ``` 56 | 57 | ```json 58 | [["a", "b"], 123] 59 | ``` 60 | ~~~ 61 | 62 | ## Maps 63 | 64 | Maps (also typically known as *dicts* or *hashmaps*) contain a set of (key, value) pairs. 65 | 66 | The syntax is similar to json. Like lists, raptor allows an optional trailing 67 | comma after the last key-value pair. 68 | 69 | ~~~admonish example title="Examples of maps" 70 | ```json 71 | {"answer": 42} 72 | ``` 73 | 74 | ```json 75 | {"name": "Heart of Gold", "engine": "Improbability drive"} 76 | ``` 77 | ~~~ 78 | -------------------------------------------------------------------------------- /src/template/escape.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::sync::Arc; 3 | 4 | use itertools::Itertools; 5 | use minijinja::{Environment, Error, ErrorKind, Value, value::ValueKind}; 6 | 7 | const fn blacklisted(ch: char) -> bool { 8 | !matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '=' | '/' | ',' | '.' | '+') 9 | } 10 | 11 | fn escape(value: &str) -> String { 12 | if value.is_empty() { 13 | return String::from("\"\""); 14 | } 15 | 16 | if !value.contains(blacklisted) { 17 | return value.into(); 18 | } 19 | 20 | let mut res = String::with_capacity(value.len() + 2); 21 | 22 | res.push('"'); 23 | 24 | for c in value.chars() { 25 | match c { 26 | '\"' => res.push_str("\\\""), 27 | '\t' => res.push_str("\\t"), 28 | '\n' => res.push_str("\\n"), 29 | '\\' => res.push_str("\\\\"), 30 | c => res.push(c), 31 | } 32 | } 33 | 34 | res.push('"'); 35 | 36 | res 37 | } 38 | 39 | fn error>>(detail: D) -> Result { 40 | Err(Error::new(ErrorKind::BadEscape, detail)) 41 | } 42 | 43 | #[allow(clippy::needless_pass_by_value)] 44 | fn escape_sh(value: Value) -> Result { 45 | match value.kind() { 46 | ValueKind::Bool => { 47 | let b: bool = value.try_into()?; 48 | Ok(b.to_string()) 49 | } 50 | ValueKind::Number => { 51 | let f: f64 = value.try_into()?; 52 | if f.is_nan() { 53 | error("Cannot escape floating point NaN")?; 54 | } 55 | if f.is_infinite() { 56 | error("Cannot escape floating point Infinity")?; 57 | } 58 | Ok(f.to_string()) 59 | } 60 | ValueKind::String => { 61 | let val: Arc = value.try_into()?; 62 | Ok(escape(&val)) 63 | } 64 | ValueKind::Seq => { 65 | let val: Vec<_> = value.try_iter()?.map(escape_sh).try_collect()?; 66 | Ok(val.join(" ")) 67 | } 68 | kind => error(format!("Cannot escape value: {value:?} of type {kind:?}"))?, 69 | } 70 | } 71 | 72 | pub fn add_filters(env: &mut Environment) { 73 | env.add_filter("sh", escape_sh); 74 | env.add_filter("escape_sh", escape_sh); 75 | } 76 | -------------------------------------------------------------------------------- /book/src/builders/part-image.md: -------------------------------------------------------------------------------- 1 | # Partition Image generator:
**`part-image`** 2 | 3 | | Mount name | Type | Usage | 4 | |:-----------|:--------|:------------------------------------------------------------------| 5 | | `input` | Overlay | The Raptor build target that will be put into the generated image | 6 | | `output` | File | Points to the resulting output file. | 7 | 8 | ## Compatibility 9 | 10 | | Target | Compatible? | 11 | |:-----------------------------|:------------| 12 | | Container: `systemd-nspawn` | ✅ | 13 | | Container: `docker` | ❌ | 14 | | Container: `podman` | ❌ | 15 | | Virtual Machine (UEFI) | ❌ | 16 | | Virtual Machine (BIOS) | ❌ | 17 | | Physical Machine (UEFI) | ❌ | 18 | | Physical Machine (BIOS) | ❌ | 19 | 20 | This builder generates partition images, containing a single filesystem. 21 | 22 | In other words, this image can be **put into** a partition, but **does not** 23 | contain a *partition table*. 24 | 25 | The primary use case is for building `systemd-nspawn` images, but this builder 26 | is also useful in other specialty applications -- for example, 27 | [efibootguard](https://github.com/siemens/efibootguard), a bootloader that 28 | supports A/B partitions for upgrade and recovery. 29 | 30 | Since efibootguard uses [Unified Kernel 31 | Images](https://github.com/siemens/efibootguard/blob/master/docs/UNIFIED-KERNEL.md), 32 | an update image typically consists of just a single filesystem. Such an image 33 | could be produced by this builder. 34 | 35 | ## Example 36 | 37 | Prerequisites: 38 | 39 | - [raptor-builders](https://github.com/chrivers/raptor-builders) is cloned to `raptor-builders` 40 | - An input target called `test.rapt` 41 | 42 | ~~~admonish note title="Raptor.toml" 43 | ```toml 44 | [raptor.link] 45 | rbuild = "raptor-builders" 46 | 47 | # Partition (filesystem) image 48 | [run.part1] 49 | target = "$rbuild.part-image" # <-- builder is specified here 50 | input = "test" 51 | output = "part.img" 52 | ``` 53 | ~~~ 54 | 55 | After this `Raptor.toml` is in place, call `raptor make` to build: 56 | 57 | ```sh 58 | sudo raptor make part1 59 | ``` 60 | 61 | When the process is complete, `part.img` will be ready for use. 62 | -------------------------------------------------------------------------------- /book/src/install.md: -------------------------------------------------------------------------------- 1 | # Install guide 2 | 3 | Currently, the only supported and recommended way to install raptor is by 4 | compiling from source. Pre-built binary packages might be available in the 5 | future. 6 | 7 | ## New user? 8 | 9 | 1. Complete [Install prerequisites](#install-prerequisites) 10 | 2. Complete [Install Raptor from git](#install-raptor-from-git-stable) 11 | 3. Enjoy Raptor 🦅 12 | 13 | ## Install prerequisites 14 | 15 | 1. Install necessary packages: 16 | ```sh 17 | sudo apt-get update 18 | sudo apt-get install -y git musl-tools 19 | ``` 20 | 21 | 2. Install [Rust 1.90 or greater](https://rustup.rs): 22 | ```sh 23 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 24 | ``` 25 | 26 | 3. Install `musl` target for rust: 27 | ```sh 28 | rustup target add x86_64-unknown-linux-musl 29 | ``` 30 | 31 | Now you are ready to install Raptor. 32 | 33 | ## Install Raptor from git \[stable\] 34 | 35 | 1. [Install prerequisites](#install-prerequisites) 36 | 37 | 2. Install `raptor` and `falcon` from git: 38 | ```sh 39 | cargo install \ 40 | --target "x86_64-unknown-linux-musl" \ 41 | --git "https://github.com/chrivers/raptor" \ 42 | raptor falcon 43 | ``` 44 | 45 | ~~~admonish warning title="Caution" 46 | The argument `--target "x86_64-unknown-linux-musl"` is very important! 47 | 48 | Without it, Raptor *will compile*, but Falcon (the sandbox client used for 49 | building containers) will not be statically compiled. Since it is running inside 50 | sandboxed environments, where glibc is not always available in the correct 51 | version (or at all), you will get confusing errors when trying to build Raptor 52 | targets. 53 | 54 | **Consequence**: without `--target "x86_64-unknown-linux-musl"`, the `cargo install` 55 | command will compile and install, but the installation *will* be broken. 56 | ~~~ 57 | 58 | ## Install Raptor from git \[development branch\] 59 | 60 | This procedure is intended for developers, beta testers, and anyone else who 61 | want to try a specific branch build of Raptor. To test a branch named 62 | `hypothetical`: 63 | 64 | 1. [Install prerequisites](#install-prerequisites) 65 | 66 | 2. Install `raptor` and `falcon` from git: 67 | ```sh 68 | cargo install \ 69 | --target "x86_64-unknown-linux-musl" \ 70 | --git "https://github.com/chrivers/raptor" \ 71 | --branch "hypothetical" \ 72 | raptor falcon 73 | ``` 74 | -------------------------------------------------------------------------------- /book/src/file-options.md: -------------------------------------------------------------------------------- 1 | # File options 2 | 3 | Several instructions (`COPY`, `WRITE`, `RENDER`, `MKDIR`) write files into the 4 | build target. They all supports common options that affect how the files are 5 | written. 6 | 7 | ~~~admonish important 8 | File options must be specified after the instruction (i.e. before 9 | source or destination path) 10 | ~~~ 11 | 12 | ## Change mode: `--chmod ` 13 | 14 | The `--chmod` option specifies the mode bits (i.e. permissions) associated with 15 | the file. The `mode` is specified as 3 or 4 octal digits. 16 | 17 | Examples: 18 | 19 | ```raptor 20 | # these are equivalent: 21 | COPY --chmod 755 script.sh /root/script.sh 22 | COPY --chmod 0755 script.sh /root/script.sh 23 | 24 | # set suid bit: 25 | COPY --chmod 4755 sudo /usr/bin/sudo 26 | 27 | # user access only, read-only: 28 | WRITE --chmod 0400 "secret" /etc/service/token 29 | ``` 30 | 31 | ```admonish tip 32 | The 3-digit version is identical to the 4-digit version, where the first digit 33 | is zero (which is a common case). 34 | For example, `755` and `0755` represent the same permissions. 35 | ``` 36 | 37 | ## Change owner: `--chown ` 38 | 39 | The `--chown` option specifies the user and/or group to own the file. 40 | 41 | | Input | User | Group | 42 | |:-------------|:------------|:------------| 43 | | `user` | `user` | (no change) | 44 | | `:` | (no change) | (no change) | 45 | | `:group` | (no change) | `group` | 46 | | `user:group` | `user` | `group` | 47 | | `user:` | `user` | `user` (!) | 48 | 49 | ```admonish important 50 | Notice the last form, where `user:` is shorthand for `user:user`. 51 | 52 | This is the same convention used by GNU coreutils, and several other programs. 53 | ``` 54 | 55 | ## Create parent directories: `-p` 56 | 57 | ```admonish note 58 | This file option is only valid for the `MKDIR` instruction. 59 | ``` 60 | 61 | The `-p` option instructs `MKDIR` to create parent directories as needed. 62 | 63 | Importantly, it also makes `MKDIR` accept existing directories, *including* the 64 | last directory. 65 | 66 | This is identical to the behavior of `-p` with the shell command `mkdir`. 67 | 68 | ```raptor 69 | # will fail if: 70 | # A) /foo is missing 71 | # or: 72 | # B) /foo/bar already exists 73 | MKDIR /foo/bar 74 | 75 | # this will create: 76 | # /foo (if missing) 77 | # and then 78 | # /foo/bar (if missing) 79 | MKDIR -p /foo/bar 80 | ``` 81 | -------------------------------------------------------------------------------- /book/src/builders/docker-image.md: -------------------------------------------------------------------------------- 1 | # Docker Image generator:
**`docker-image`** 2 | 3 | | Mount name | Type | Usage | 4 | |:--------------------------|:-------|:---------------------------------------------------------------------------------------------| 5 | | `input` | Layers | The Raptor build target to genrate as a docker image. | 6 | | `output` | File | Points to the resulting docker image file. | 7 | | `cache`
**(Optional)** | Simple | Cache for tar files of built layers.
Will save time when repeating layers between builds. | 8 | 9 | This builder generates a Docker image from a Raptor target. 10 | 11 | Each layer in Raptor (one for the target, plus one for each `FROM`) becomes a 12 | Docker layer in the generated image. This allows shared Raptor layers to stay 13 | shared when converted to Docker images. 14 | 15 | ## Compatibility 16 | 17 | | Target | Compatible? | 18 | |:----------------------------|:------------| 19 | | Container: `systemd-nspawn` | ❌ | 20 | | Container: `docker` | ✅ | 21 | | Container: `podman` | ✅ | 22 | | Virtual Machine (UEFI) | ❌ | 23 | | Virtual Machine (BIOS) | ❌ | 24 | | Physical Machine (UEFI) | ❌ | 25 | | Physical Machine (BIOS) | ❌ | 26 | 27 | ## Example 28 | 29 | Prerequisites: 30 | 31 | - [raptor-builders](https://github.com/chrivers/raptor-builders) is cloned to `raptor-builders` 32 | - An input target called `test.rapt` 33 | 34 | ~~~admonish note title="Raptor.toml" 35 | ```toml 36 | [raptor.link] 37 | rbuild = "raptor-builders" 38 | 39 | # Docker image 40 | [run.docker1] 41 | target = "$rbuild.docker-image" # <-- builder is specified here 42 | input = ["test"] 43 | output = "test.tar" 44 | ## to speed up builds with shared layers, 45 | ## specify a cache directory: 46 | # cache = "/tmp/docker-image-cache" 47 | ``` 48 | ~~~ 49 | 50 | After this `Raptor.toml` is in place, call `raptor make` to build: 51 | 52 | ```sh 53 | sudo raptor make docker1 54 | ``` 55 | 56 | When the process is complete, `test.tar` will be ready for use. 57 | 58 | It can be imported: 59 | 60 | ```sh 61 | # import to Docker 62 | docker load -i test.tar 63 | 64 | # import to Podman 65 | podman load -i test.tar 66 | ``` 67 | -------------------------------------------------------------------------------- /src/build/stats.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use crate::RaptorResult; 4 | use crate::build::BuildTarget; 5 | use raptor_parser::ast::FromSource; 6 | 7 | pub struct BuildTargetStats { 8 | pub targets: HashMap, 9 | pub roots: HashSet, 10 | pub map: HashMap, 11 | pub rmap: HashMap>, 12 | } 13 | 14 | impl Default for BuildTargetStats { 15 | fn default() -> Self { 16 | Self::new() 17 | } 18 | } 19 | 20 | impl BuildTargetStats { 21 | #[must_use] 22 | pub fn new() -> Self { 23 | Self { 24 | targets: HashMap::new(), 25 | roots: HashSet::new(), 26 | map: HashMap::new(), 27 | rmap: HashMap::new(), 28 | } 29 | } 30 | 31 | pub fn merge(&mut self, stack: Vec) -> RaptorResult<()> { 32 | for layer in stack { 33 | match layer { 34 | BuildTarget::Program(ref program) => { 35 | let name = program.path.file_stem().unwrap().to_string(); 36 | 37 | if let Some((from, _origin)) = program.from() { 38 | let key = match &from { 39 | FromSource::Raptor(from) => from.to_string(), 40 | FromSource::Docker(image) => { 41 | if image.contains('/') { 42 | format!("docker://{image}") 43 | } else { 44 | format!("docker://library/{image}") 45 | } 46 | } 47 | }; 48 | 49 | self.rmap 50 | .entry(key.clone()) 51 | .or_default() 52 | .insert(name.clone()); 53 | self.map.insert(name.clone(), key); 54 | } else { 55 | self.roots.insert(name.clone()); 56 | } 57 | self.targets.insert(name, layer); 58 | } 59 | BuildTarget::DockerSource(ref src) => { 60 | let name = format!("docker://{src}"); 61 | self.targets.insert(name.clone(), layer); 62 | self.roots.insert(name); 63 | } 64 | } 65 | } 66 | 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /book/src/builders/disk-image.md: -------------------------------------------------------------------------------- 1 | # Disk Image generator:
**`disk-image`** 2 | 3 | | Mount name | Type | Usage | 4 | |:-----------|:--------|:-----------------------------------------------------------------------------------------------------------------------| 5 | | `input` | Overlay | The Raptor build target that will be put into the generated image | 6 | | `output` | File | Points to the resulting output file. | 7 | 8 | ## Compatibility 9 | 10 | | Target | Compatible? | 11 | |:-----------------------------|:--------------------| 12 | | Container: `systemd-nspawn` | ✅ (`raw`) | 13 | | Container: `docker` | ❌ | 14 | | Container: `podman` | ❌ | 15 | | Virtual Machine (UEFI) | ✅ (`raw`, `qcow2`) | 16 | | Virtual Machine (BIOS) | ❌ | 17 | | Physical Machine (UEFI) | ✅ (`raw`) | 18 | | Physical Machine (BIOS) | ❌ | 19 | 20 | This builder generates disk images, including a partition table, and separate 21 | partitions for `/`, `/boot` and `/boot/efi`. 22 | 23 | It uses the Discoverable Partitions Specification[^dps] to make the images 24 | compatible with both physical hardware, virtual machines, and `systemd-nspawn`. 25 | 26 | [^dps]: [Discoverable Partitions Specification](https://uapi-group.org/specifications/specs/discoverable_partitions_specification/) 27 | 28 | ## Example 29 | 30 | Prerequisites: 31 | 32 | - [raptor-builders](https://github.com/chrivers/raptor-builders) is cloned to `raptor-builders` 33 | - An input target called `test.rapt` 34 | 35 | ~~~admonish note title="Raptor.toml" 36 | ```toml 37 | [raptor.link] 38 | rbuild = "raptor-builders" 39 | 40 | # Disk image (raw) 41 | [run.disk1] 42 | target = "$rbuild.disk-image" # <-- builder is specified here 43 | input = ["test"] 44 | output = "disk.img" 45 | ## implied default: 46 | ## env.OUTPUT_FORMAT = "raw" 47 | 48 | # Disk image (qcow2) 49 | [run.disk2] 50 | target = "$rbuild.disk-image" # <-- builder is specified here 51 | input = ["test"] 52 | output = "disk.qcow2" 53 | env.OUTPUT_FORMAT = "qcow2" 54 | ``` 55 | ~~~ 56 | 57 | After this `Raptor.toml` is in place, call `raptor make` to build: 58 | 59 | ```sh 60 | sudo raptor make disk1 disk2 61 | ``` 62 | 63 | When the process is complete, `disk.img` and `disk.qcow2` will be ready for use. 64 | -------------------------------------------------------------------------------- /book/src/intro.md: -------------------------------------------------------------------------------- 1 | # Getting started with Raptor 2 | 3 | ## What is Raptor? 4 | 5 | ![raptor logo](images/logo-title.png) 6 | 7 | Raptor[^raptor] is a modern, fast, and easy-to-use system for building disk images, 8 | bootable isos, containers and much more - all from a simple, Dockerfile-inspired 9 | syntax. 10 | 11 | It uses `systemd-nspawn` for sandboxing when building or running containers. 12 | 13 | ~~~admonish tip title="~Eagle..~ err, eager to get started?" 14 | Start by [installing raptor](install.md), then head over to the 15 | [Debian Liveboot walkthrough](walkthrough/debian/index.md) to get a 16 | hands-on introduction to building a bootable iso. 17 | ~~~ 18 | 19 | ## Theory of operation 20 | 21 | Raptor builds *layers* from *`.rapt`* files. If you are familiar with Docker, 22 | this is similar to how Docker builds *containers* from a *`Dockerfile`*. 23 | 24 | ~~~pikchr 25 | {{#include images/raptor-layers.pikchr}} 26 | ~~~ 27 | 28 | However, Raptor has a different scope, and can do considerably different things 29 | than Docker. 30 | 31 | ~~~admonish warning title="Heads up!" 32 | The entire Raptor project, including this book, the program itself, and the 33 | companion project [raptor-builders](https://github.com/chrivers/raptor-builders), is 34 | still quite young. 35 | 36 | If you find (or suspect) any bugs, [please report 37 | them](https://github.com/chrivers/raptor/issues) so everybody can benefit. 38 | 39 | At this point, Raptor has reached a stage where breaking changes are rare, but 40 | we don't yet make any particular guarantees. We will **try our best** to announce 41 | major changes clearly, and ahead of time. 42 | 43 | If you have questions, ideas or feedback, don't hesitate to [join the 44 | discussion](https://github.com/chrivers/raptor/discussions). 45 | ~~~ 46 | 47 | ## Syntax 48 | 49 | Raptor uses a syntax similar to `Dockerfile`. Statements start with uppercase 50 | keywords, and are terminated by end of line. 51 | 52 | Anything after `#` is treated as a comment 53 | 54 | ```raptor 55 | # This copies "foo" from the host to "/bar" inside the build target 56 | COPY file1 /file1 57 | 58 | COPY file2 /file2 # comments can appear here too 59 | ``` 60 | 61 | ## Raptor files 62 | 63 | Before being parsed as raptor files, `.rapt` files are processed through 64 | [minijinja](https://github.com/mitsuhiko/minijinja), a powerful templating 65 | language. 66 | 67 | [^raptor]: [According to wikipedia](https://en.wikipedia.org/wiki/Raptor): "The word "raptor" refers to several groups of avian and non-avian dinosaurs which primarily capture and subdue/kill prey with their talons.". Hopefully, this Raptor is less scary. 68 | -------------------------------------------------------------------------------- /book/src/mount-types.md: -------------------------------------------------------------------------------- 1 | # Mount types 2 | 3 | ```admonish tip 4 | For an introduction to mounts, see the [MOUNT](/inst/mount.md) referrence. 5 | ``` 6 | 7 | When running containers, Raptor supports mounting various file resources into 8 | the container namespace. 9 | 10 | In the simplest form, this means presenting a directory from the host 11 | environment, at a specified location in the container. 12 | 13 | This is equivalent to how Docker uses volume mounts (`-v` / `--volume`). 14 | 15 | However, Raptor supports more than just mounting directories: 16 | 17 | | Type | Description | Access | 18 | |:----------------------|:----------------------------------------------|:-----------| 19 | | `MOUNT --simple ...` | Mounts a directory from the host (default) | Read/write | 20 | | `MOUNT --file ...` | Mounts a single file from the host | Read/write | 21 | | `MOUNT --layers ...` | Mounts a view of set of layers as directories | Read-only | 22 | | `MOUNT --overlay ...` | Mounts a view of the sum of a set of layers | Read-only | 23 | 24 | ~~~admonish tip 25 | If no mount type is specified, `--simple` is implied as the default. 26 | 27 | For clarity, it is recommended to always specify a mount type. 28 | ~~~ 29 | 30 | Read more about the specific mount types: 31 | 32 | - [`--simple`](mount-types/simple.md) 33 | - [`--file`](mount-types/file.md) 34 | - [`--layers`](mount-types/layers.md) 35 | - [`--overlay`](mount-types/overlay.md) 36 | 37 | ## Options 38 | 39 | All mount types take two optional arguments: 40 | 41 | | Argument | Description | 42 | |:--------------|:---------------------------------------------------------------------------------------------------| 43 | | `--readonly` | Mount the file or directory read-only.
(for `--layers`/`--overlay` this is **always implied**). | 44 | | `--readwrite` | Mount the file or directory read-write.
(for `--layers`/`--overlay` this is **invalid**). | 45 | | `--optional` | Allow starting the container, **even if** this mount is **not specified**. | 46 | | `--required` | Deny starting the container, **unless** this mount **is specified**. | 47 | 48 | ## Examples 49 | 50 | ```raptor 51 | # Both options can be combined: 52 | MOUNT --readonly --optional --simple input /input 53 | 54 | # Optional layers mount: 55 | MOUNT --optional --layers input /data 56 | 57 | # Later --required overrides previous --optional: 58 | MOUNT --optional --required --layers input /data 59 | ``` 60 | -------------------------------------------------------------------------------- /src/template/mod.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod escape; 3 | mod file; 4 | mod header; 5 | mod load_yaml; 6 | mod log; 7 | 8 | use std::borrow::Cow; 9 | 10 | use minijinja::syntax::SyntaxConfig; 11 | use minijinja::{Environment, Error, ErrorKind, UndefinedBehavior}; 12 | 13 | use crate::RaptorResult; 14 | 15 | trait AdaptError { 16 | fn adapt_err(self, msg: impl Into>) -> Result; 17 | } 18 | 19 | impl AdaptError for Result 20 | where 21 | E: std::error::Error + Send + Sync + 'static, 22 | { 23 | fn adapt_err(self, msg: impl Into>) -> Result { 24 | self.map_err(|err| Error::new(ErrorKind::InvalidOperation, msg).with_source(err)) 25 | } 26 | } 27 | 28 | impl AdaptError for Option { 29 | fn adapt_err(self, msg: impl Into>) -> Result { 30 | self.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, msg)) 31 | } 32 | } 33 | 34 | pub fn make_environment<'a>() -> RaptorResult> { 35 | let mut env = Environment::empty(); 36 | env.set_debug(true); 37 | env.set_keep_trailing_newline(true); 38 | env.set_undefined_behavior(UndefinedBehavior::Strict); 39 | 40 | env.set_loader(|name| { 41 | Ok(Some( 42 | std::fs::read_to_string(name) 43 | .map(|mut data| { 44 | // file must end in newline, to avoid minijinja 45 | // glueing the included file together incorrectly 46 | if !data.ends_with('\n') { 47 | data.push('\n'); 48 | } 49 | data 50 | }) 51 | .map_err(|e| { 52 | Error::new( 53 | ErrorKind::BadInclude, 54 | format!("Could not open [{name}]: {e}"), 55 | ) 56 | })?, 57 | )) 58 | }); 59 | 60 | env.set_path_join_callback(|name, parent| { 61 | let mut rv = parent.split('/').collect::>(); 62 | rv.pop(); 63 | name.split('/').for_each(|segment| match segment { 64 | "." => {} 65 | ".." => drop(rv.pop()), 66 | _ => rv.push(segment), 67 | }); 68 | rv.join("/").into() 69 | }); 70 | 71 | env.set_syntax( 72 | SyntaxConfig::builder() 73 | .line_statement_prefix("$ ") 74 | .line_comment_prefix("#") 75 | .build()?, 76 | ); 77 | 78 | log::add_functions(&mut env); 79 | file::add_functions(&mut env); 80 | args::add_functions(&mut env); 81 | load_yaml::add_functions(&mut env); 82 | header::add_functions(&mut env); 83 | escape::add_filters(&mut env); 84 | 85 | Ok(env) 86 | } 87 | -------------------------------------------------------------------------------- /book/theme/highlight.css: -------------------------------------------------------------------------------- 1 | pre code.hljs { 2 | display: block; 3 | overflow-x: auto; 4 | padding: 1em 5 | } 6 | code.hljs { 7 | padding: 3px 5px 8 | } 9 | /*! 10 | Theme: GitHub Dark 11 | Description: Dark theme as seen on github.com 12 | Author: github.com 13 | Maintainer: @Hirse 14 | Updated: 2021-05-15 15 | 16 | Outdated base version: https://github.com/primer/github-syntax-dark 17 | Current colors taken from GitHub's CSS 18 | */ 19 | .hljs { 20 | color: #c9d1d9; 21 | background: #0d1117 22 | } 23 | .hljs-doctag, 24 | .hljs-keyword, 25 | .hljs-meta .hljs-keyword, 26 | .hljs-template-tag, 27 | .hljs-template-variable, 28 | .hljs-type, 29 | .hljs-variable.language_ { 30 | /* prettylights-syntax-keyword */ 31 | color: #ff7b72 32 | } 33 | .hljs-title, 34 | .hljs-title.class_, 35 | .hljs-title.class_.inherited__, 36 | .hljs-title.function_ { 37 | /* prettylights-syntax-entity */ 38 | color: #d2a8ff 39 | } 40 | .hljs-attr, 41 | .hljs-attribute, 42 | .hljs-literal, 43 | .hljs-meta, 44 | .hljs-number, 45 | .hljs-operator, 46 | .hljs-variable, 47 | .hljs-selector-attr, 48 | .hljs-selector-class, 49 | .hljs-selector-id { 50 | /* prettylights-syntax-constant */ 51 | color: #79c0ff 52 | } 53 | .hljs-regexp, 54 | .hljs-string, 55 | .hljs-meta .hljs-string { 56 | /* prettylights-syntax-string */ 57 | color: #a5d6ff 58 | } 59 | .hljs-built_in, 60 | .hljs-symbol { 61 | /* prettylights-syntax-variable */ 62 | color: #ffa657 63 | } 64 | .hljs-comment, 65 | .hljs-code, 66 | .hljs-formula { 67 | /* prettylights-syntax-comment */ 68 | color: #8b949e 69 | } 70 | .hljs-name, 71 | .hljs-quote, 72 | .hljs-selector-tag, 73 | .hljs-selector-pseudo { 74 | /* prettylights-syntax-entity-tag */ 75 | color: #7ee787 76 | } 77 | .hljs-subst { 78 | /* prettylights-syntax-storage-modifier-import */ 79 | color: #c9d1d9 80 | } 81 | .hljs-section { 82 | /* prettylights-syntax-markup-heading */ 83 | color: #1f6feb; 84 | font-weight: bold 85 | } 86 | .hljs-bullet { 87 | /* prettylights-syntax-markup-list */ 88 | color: #f2cc60 89 | } 90 | .hljs-emphasis { 91 | /* prettylights-syntax-markup-italic */ 92 | color: #c9d1d9; 93 | font-style: italic 94 | } 95 | .hljs-strong { 96 | /* prettylights-syntax-markup-bold */ 97 | color: #c9d1d9; 98 | font-weight: bold 99 | } 100 | .hljs-addition { 101 | /* prettylights-syntax-markup-inserted */ 102 | color: #aff5b4; 103 | background-color: #033a16 104 | } 105 | .hljs-deletion { 106 | /* prettylights-syntax-markup-deleted */ 107 | color: #ffdcd7; 108 | background-color: #67060c 109 | } 110 | .hljs-char.escape_, 111 | .hljs-link, 112 | .hljs-params, 113 | .hljs-property, 114 | .hljs-punctuation, 115 | .hljs-tag { 116 | /* purposely ignored */ 117 | 118 | } -------------------------------------------------------------------------------- /tests/diagnostic.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io::Read; 3 | use std::path::PathBuf; 4 | use std::process::{Command, Stdio}; 5 | 6 | use camino::Utf8Path; 7 | use libtest_mimic::{Arguments, Trial}; 8 | use pretty_assertions::assert_eq; 9 | use raptor::RaptorResult; 10 | 11 | // The function `cargo_dir` is copied from cargo source code (dual Apache / MIT license) 12 | pub fn cargo_dir() -> PathBuf { 13 | env::var_os("CARGO_BIN_PATH") 14 | .map(PathBuf::from) 15 | .or_else(|| { 16 | env::current_exe().ok().map(|mut path| { 17 | path.pop(); 18 | if path.ends_with("deps") { 19 | path.pop(); 20 | } 21 | path 22 | }) 23 | }) 24 | .unwrap_or_else(|| panic!("CARGO_BIN_PATH wasn't set. Cannot continue running test")) 25 | } 26 | 27 | fn run_raptor(filename: &Utf8Path) -> RaptorResult { 28 | const SHOULD_BUILD: &[&str] = &["error_failing_instruction"]; 29 | 30 | let should_build = SHOULD_BUILD.contains(&filename.file_name().unwrap_or_default()); 31 | 32 | let raptor = cargo_dir().join("raptor"); 33 | 34 | let mut cmd = if should_build { 35 | let mut cmd = Command::new("sudo"); 36 | cmd.arg(raptor).arg("build"); 37 | cmd 38 | } else { 39 | let mut cmd = Command::new(raptor); 40 | cmd.arg("build").arg("-n"); 41 | cmd 42 | }; 43 | 44 | cmd.arg("-qqq") 45 | .arg(filename) 46 | .stdin(Stdio::null()) 47 | .stdout(Stdio::null()) 48 | .stderr(Stdio::piped()); 49 | 50 | let mut proc = cmd.spawn()?; 51 | let mut stderr = proc.stderr.take().unwrap(); 52 | let mut message = String::new(); 53 | stderr.read_to_string(&mut message)?; 54 | 55 | Ok(message) 56 | } 57 | 58 | #[allow(clippy::case_sensitive_file_extension_comparisons)] 59 | fn main() -> RaptorResult<()> { 60 | let args = Arguments::from_args(); 61 | 62 | let mut tests = vec![]; 63 | 64 | let cases = Utf8Path::new("tests/cases/error").read_dir_utf8()?; 65 | 66 | for dent in cases { 67 | let dent = dent?; 68 | let file_name = dent.file_name(); 69 | if !file_name.ends_with(".rapt") || !file_name.starts_with("error_") { 70 | continue; 71 | } 72 | let refpath = dent.path().with_extension("out"); 73 | let srcpath = dent.path().with_extension(""); 74 | 75 | let test = Trial::test(dent.file_name(), move || { 76 | let refdata = std::fs::read_to_string(refpath)?; 77 | let newdata = run_raptor(&srcpath)?; 78 | 79 | assert_eq!(refdata, newdata); 80 | Ok(()) 81 | }); 82 | 83 | tests.push(test); 84 | } 85 | 86 | /* run all tests using libtest-mimic */ 87 | libtest_mimic::run(&args, tests).exit() 88 | } 89 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/print/mod.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8Path; 2 | use colored::Colorize; 3 | 4 | use std::fmt::{Debug, Formatter, Result}; 5 | 6 | use crate::ast::{Chown, FromSource, IncludeArg, InstEnvAssign}; 7 | 8 | pub trait Theme { 9 | fn keyword(&mut self, name: &str) -> Result; 10 | fn chmod(&mut self, chmod: &Option) -> Result; 11 | fn chown(&mut self, chown: &Option) -> Result; 12 | fn from(&mut self, src: &FromSource) -> Result; 13 | fn src(&mut self, src: &Utf8Path) -> Result; 14 | fn dest(&mut self, dest: &Utf8Path) -> Result; 15 | fn include_arg(&mut self, arg: &IncludeArg) -> Result; 16 | fn env_arg(&mut self, arg: &InstEnvAssign) -> Result; 17 | fn name(&mut self, name: &str) -> Result; 18 | fn value(&mut self, value: impl Debug) -> Result; 19 | } 20 | 21 | impl Theme for Formatter<'_> { 22 | fn keyword(&mut self, name: &str) -> Result { 23 | write!(self, "{}", name.bright_blue()) 24 | } 25 | 26 | fn chmod(&mut self, chmod: &Option) -> Result { 27 | if let Some(chmod) = chmod { 28 | write!( 29 | self, 30 | " {} {}", 31 | "--chmod".bright_white(), 32 | format!("{chmod:04o}").cyan() 33 | )?; 34 | } 35 | Ok(()) 36 | } 37 | 38 | fn chown(&mut self, chown: &Option) -> Result { 39 | if let Some(chown) = chown { 40 | write!( 41 | self, 42 | " {} {}", 43 | "--chown".bright_white(), 44 | format!("{chown}").cyan() 45 | )?; 46 | } 47 | Ok(()) 48 | } 49 | 50 | fn from(&mut self, src: &FromSource) -> Result { 51 | write!(self, " {}", format!("{src}").green()) 52 | } 53 | 54 | fn src(&mut self, src: &Utf8Path) -> Result { 55 | write!(self, " {}", format!("{src:?}").green()) 56 | } 57 | 58 | fn dest(&mut self, dest: &Utf8Path) -> Result { 59 | write!(self, " {}", format!("{dest:?}").bright_green()) 60 | } 61 | 62 | fn include_arg(&mut self, arg: &IncludeArg) -> Result { 63 | write!( 64 | self, 65 | " {}{}{}", 66 | arg.name.yellow(), 67 | "=".dimmed(), 68 | format!("{}", arg.value).red() 69 | ) 70 | } 71 | 72 | fn env_arg(&mut self, arg: &InstEnvAssign) -> Result { 73 | write!( 74 | self, 75 | " {}{}{}", 76 | arg.key.yellow(), 77 | "=".dimmed(), 78 | format!("{:?}", arg.value).red() 79 | ) 80 | } 81 | 82 | fn name(&mut self, name: &str) -> Result { 83 | write!(self, " {}", name.yellow()) 84 | } 85 | 86 | fn value(&mut self, value: impl Debug) -> Result { 87 | write!(self, " {}", format!("{value:?}").bright_red()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/raptor-parser/src/ast/include.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Debug, Display}; 2 | 3 | use camino::Utf8Path; 4 | use minijinja::Value; 5 | use serde::Serialize; 6 | 7 | use crate::ast::Origin; 8 | use crate::print::Theme; 9 | use crate::util::module_name::ModuleName; 10 | 11 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 12 | pub struct Lookup { 13 | pub path: ModuleName, 14 | pub origin: Origin, 15 | } 16 | 17 | impl Lookup { 18 | #[must_use] 19 | pub const fn new(path: ModuleName, origin: Origin) -> Self { 20 | Self { path, origin } 21 | } 22 | } 23 | 24 | impl Display for Lookup { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | write!(f, "{}", &self.path) 27 | } 28 | } 29 | 30 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 31 | pub enum Expression { 32 | Lookup(Lookup), 33 | Value(Value), 34 | } 35 | 36 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 37 | pub struct IncludeArg { 38 | pub name: String, 39 | pub value: Expression, 40 | } 41 | 42 | impl IncludeArg { 43 | pub fn make(name: impl AsRef, value: Expression) -> Self { 44 | Self { 45 | name: name.as_ref().to_string(), 46 | value, 47 | } 48 | } 49 | 50 | pub fn lookup(name: impl AsRef, path: &[impl ToString], origin: Origin) -> Self { 51 | Self { 52 | name: name.as_ref().to_string(), 53 | value: Expression::Lookup(Lookup { 54 | path: ModuleName::new(path.iter().map(ToString::to_string).collect()), 55 | origin, 56 | }), 57 | } 58 | } 59 | 60 | pub fn value(name: impl AsRef, value: impl Serialize) -> Self { 61 | Self { 62 | name: name.as_ref().to_string(), 63 | value: Expression::Value(Value::from_serialize(value)), 64 | } 65 | } 66 | } 67 | 68 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 69 | pub struct InstInclude { 70 | pub src: ModuleName, 71 | pub args: Vec, 72 | } 73 | 74 | impl Display for IncludeArg { 75 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 76 | write!(f, "{}={}", self.name, self.value)?; 77 | Ok(()) 78 | } 79 | } 80 | 81 | impl Display for Expression { 82 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 83 | match self { 84 | Self::Lookup(l) => write!(f, "{l}"), 85 | Self::Value(v) => write!(f, "{v:?}"), 86 | } 87 | } 88 | } 89 | 90 | impl Display for InstInclude { 91 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 92 | f.keyword("INCLUDE")?; 93 | f.src(Utf8Path::new(&self.src.to_string()))?; 94 | for arg in &self.args { 95 | f.include_arg(arg)?; 96 | } 97 | Ok(()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /book/src/builders/deblive.md: -------------------------------------------------------------------------------- 1 | # Debian Liveboot iso generator:
**`deblive`** 2 | 3 | ~~~admonish important 4 | **This builder requires an input from the Debian family.** 5 | 6 | It should work for Debian derivatives (Ubuntu, etc), as long as the 7 | prerequisite packages are installed. 8 | ~~~ 9 | 10 | This builder generates an `iso` file suitable for live booting. All layers are 11 | packed into read-only squashfs files, which are mounted using overlayfs, on 12 | boot. 13 | 14 | | Mount | Type | Usage | 15 | |:---------|:-------|:---------------------------------------------------------------------------------------| 16 | | `cache` | Simple | Contains cache of previously built `.squashfs` files.
(to avoid expensive rebuilds) | 17 | | `input` | Layers | The Raptor build target(s) that will be put on the iso | 18 | | `output` | File | Points to the resulting output file. | 19 | 20 | ~~~admonish tip 21 | This builder has a 📕 [detailed walkthrough](../walkthrough/debian/). 22 | ~~~ 23 | 24 | ![example screenshot](../images/deblive-grub.png) 25 | 26 | If multiple targets are specified, each will get its own GRUB menu entry in the 27 | boot menu. 28 | 29 | The menu order will be the same as the order the `input` targets are specified 30 | in, and the first input will be the default boot option. 31 | 32 | ## Compatibility 33 | 34 | | Target | Compatible? | 35 | |:-----------------------------|:------------| 36 | | Container: `systemd-nspawn` | ❌ | 37 | | Container: `docker` | ❌ | 38 | | Container: `podman` | ❌ | 39 | | Virtual Machine (UEFI) | ✅ | 40 | | Virtual Machine (BIOS) | ✅ | 41 | | Physical Machine (UEFI) | ✅ | 42 | | Physical Machine (BIOS) | ✅ | 43 | 44 | Debian liveboot ISOs are widely compatible with both physical and virtual 45 | machines, including UEFI and BIOS-based platforms. 46 | 47 | The `deblive` builder is not compatible with `systemd-nspawn`, since liveboot 48 | requires `initrd` support to perform `overlayfs` mounting. 49 | 50 | ## Example 51 | 52 | Prerequisites: 53 | 54 | - [raptor-builders](https://github.com/chrivers/raptor-builders) is cloned to `raptor-builders` 55 | - An input target called `test.rapt` 56 | 57 | ~~~admonish note title="Raptor.toml" 58 | ```toml 59 | [raptor.link] 60 | rbuild = "raptor-builders" 61 | 62 | [run.iso1] 63 | target = "$rbuild.deblive" # <-- builder is specified here 64 | cache = "cache2" 65 | input = ["test"] 66 | output = "live.iso" 67 | env.GRUB_TIMEOUT = "0" 68 | ``` 69 | ~~~ 70 | 71 | After this `Raptor.toml` is in place, call `raptor make` to build: 72 | 73 | ```sh 74 | sudo raptor make iso1 75 | ``` 76 | 77 | When the process is complete, `live.iso` will be ready for use. 78 | -------------------------------------------------------------------------------- /book/src/builders/environment.md: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | 3 | Several settings can be adjusted via environment variables. These can be specified either on the command line: 4 | 5 | ```sh 6 | sudo raptor run -e OUTPUT_FORMAT=qcow2 ... 7 | ``` 8 | 9 | or in `Raptor.toml`: 10 | 11 | ```toml 12 | [run.example-target] 13 | target = "example" 14 | 15 | # notice the `env.` prefix! 16 | env.OUTPUT_FORMAT = "qcow2" 17 | ``` 18 | 19 | ## Environment reference 20 | 21 | | Variable | `deblive` | `live-disk-image` | `disk-image` | `part-image` | 22 | |:----------------|-----------|-------------------|--------------|--------------| 23 | | `GRUB_TIMEOUT` | ✅ | ✅ | ❌ | ❌ | 24 | | `OUTPUT_FORMAT` | ❌ | ✅ | ✅ | ✅ | 25 | | `EFI_MB` | ❌ | ✅ | ✅ | ❌ | 26 | | `BOOT_MB` | ❌ | ✅ | ✅ | ❌ | 27 | | `FREE_MB` | ❌ | ✅ | ✅ | ✅ | 28 | | `USED_MB` | ❌ | ✅ | ✅ | ✅ | 29 | | `SIZE_MB` | ❌ | ✅ | ✅ | ✅ | 30 | 31 | ### **`GRUB_TIMEOUT`** 32 | 33 | - Default: `5` 34 | - Supported by: `deblive`, `live-disk-image` 35 | 36 | The auto-generated GRUB boot menu will use `GRUB_TIMEOUT` as the countdown until 37 | automatically booting the first target. 38 | 39 | Setting this to `0` will boot without showing the GRUB menu. 40 | 41 | ### **`OUTPUT_FORMAT`** 42 | 43 | - Default: `raw` 44 | - Supported by: `live-disk-image`, `disk-image`, `part-image` 45 | 46 | Output format of the image. Common values include `raw` and `qcow2`. 47 | 48 | See `qemu-img --help` for the complete list 49 | 50 | ### **`EFI_MB`** 51 | 52 | - Default: `32` 53 | - Supported by: `live-disk-image`, `disk-image` 54 | 55 | Size of ESP, the EFI System Partition (`/boot/efi`). 56 | 57 | ### **`BOOT_MB`** 58 | 59 | - Default: `512` 60 | - Supported by: `live-disk-image`, `disk-image` 61 | 62 | Size of the boot partition (`/boot`) 63 | 64 | ### **`FREE_MB`** 65 | 66 | - Default: `512` 67 | - Supported by: `live-disk-image`, `disk-image`, `part-image` 68 | 69 | How much free space to target in the resulting image. 70 | 71 | ### **`USED_MB`** 72 | 73 | - Default: **none** 74 | - Supported by: `live-disk-image`, `disk-image`, `part-image` 75 | 76 | Total disk space of all files in the root filesystem. 77 | 78 | Calculated when building the image, unless specified manually. 79 | 80 | ### **`SIZE_MB`** 81 | 82 | - Default: **none** 83 | - Supported by: `live-disk-image`, `disk-image`, `part-image` 84 | 85 | The size of the resulting image. 86 | 87 | Calculated as: 88 | - `live-disk-image`: `EFI_MB + BOOT_MB + USED_MB + FREE_MB` 89 | - `disk-image`: `EFI_MB + BOOT_MB + USED_MB + FREE_MB` 90 | - `part-image`: `USED_MB + FREE_MB` 91 | 92 | unless specified manually for exact control. 93 | -------------------------------------------------------------------------------- /book/src/builders/live-disk-image.md: -------------------------------------------------------------------------------- 1 | # Debian Liveboot Disk Image generator:
**`live-disk-image`** 2 | 3 | ~~~admonish important 4 | **This builder requires an input from the Debian family.** 5 | 6 | It should work for Debian derivatives (Ubuntu, etc), as long as the 7 | prerequisite packages are installed. 8 | ~~~ 9 | 10 | | Mount name | Type | Usage | 11 | |:-----------|:-------|:---------------------------------------------------------------------------------------| 12 | | `cache` | Simple | Contains cache of previously built `.squashfs` files.
(to avoid expensive rebuilds) | 13 | | `input` | Layers | The Raptor build target(s) that will be put on the generated image | 14 | | `output` | File | Points to the resulting output file. | 15 | 16 | ## Compatibility 17 | 18 | | Target | Compatible? | 19 | |:----------------------------|:--------------------| 20 | | Container: `systemd-nspawn` | ❌ | 21 | | Container: `docker` | ❌ | 22 | | Container: `podman` | ❌ | 23 | | Virtual Machine (UEFI) | ✅ (`raw`, `qcow2`) | 24 | | Virtual Machine (BIOS) | ❌ | 25 | | Physical Machine (UEFI) | ✅ (`raw`) | 26 | | Physical Machine (BIOS) | ❌ | 27 | 28 | This builder also generates Debian Liveboot image, but instead of generating a 29 | `.iso` file, it generates a disk image, including a partition table, and 30 | separate partitions for `/`, `/boot` and `/boot/efi`. 31 | 32 | The result is a disk image that allows a physical or virtual machine to boot 33 | normally, but with the root file system mounted as `overlayfs` backed by the 34 | `squashfs` files for each layer. 35 | 36 | It uses the Discoverable Partitions Specification[^dps] to make the images 37 | compatible with both physical hardware, virtual machines, and `systemd-nspawn`. 38 | 39 | [^dps]: [Discoverable Partitions Specification](https://uapi-group.org/specifications/specs/discoverable_partitions_specification/) 40 | 41 | ## Example 42 | 43 | Prerequisites: 44 | 45 | - [raptor-builders](https://github.com/chrivers/raptor-builders) is cloned to `raptor-builders` 46 | - An input target called `test.rapt` 47 | 48 | ~~~admonish note title="Raptor.toml" 49 | ```toml 50 | [raptor.link] 51 | rbuild = "raptor-builders" 52 | 53 | # Live disk image (raw) 54 | [run.live1] 55 | target = "$rbuild.live-disk-image" # <-- builder is specified here 56 | cache = "cache2" 57 | input = ["test"] 58 | output = "live.img" 59 | ## implied default: 60 | ## env.GRUB_TIMEOUT = "5" 61 | ## env.OUTPUT_FORMAT = "raw" 62 | 63 | # Live disk image (qcow2, no boot delay) 64 | [run.live2] 65 | target = "$rbuild.live-disk-image" # <-- builder is specified here 66 | cache = "cache2" 67 | input = ["test"] 68 | output = "live.qcow2" 69 | env.GRUB_TIMEOUT = "0" 70 | env.OUTPUT_FORMAT = "qcow2" 71 | ``` 72 | ~~~ 73 | 74 | After this `Raptor.toml` is in place, call `raptor make` to build: 75 | 76 | ```sh 77 | sudo raptor make live1 live2 78 | ``` 79 | 80 | When the process is complete, `live.img` and `live.qcow2` will be ready for use. 81 | -------------------------------------------------------------------------------- /book/src/inst/mount.md: -------------------------------------------------------------------------------- 1 | # Instruction `MOUNT` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | MOUNT [--] 6 | ``` 7 | ~~~ 8 | 9 | ```admonish important title="Build-time instruction" 10 | The `MOUNT` instructions only affects *running* a container, not *building* a 11 | container. 12 | ``` 13 | 14 | ```admonish tip 15 | See the section on [mount types](mount-types.md). 16 | ``` 17 | 18 | By using `MOUNT`, targets can specify certain resources (files, directories, 19 | layers, etc) that should be made available when running the container. 20 | 21 | Raptor mounts are identified by a `name`, which is used when running the 22 | container, to declare what to mount. 23 | 24 | When running a raptor container, a mount input is specified with the `-M 25 | ` command line option. 26 | 27 | ~~~admonish tip 28 | The syntax `-M ` can be a bit unwieldy. 29 | 30 | Since certain mount names are very common, they have a shorter syntax available: 31 | 32 | | Name | Long form | Short form | 33 | |:-------|:------------------|:-----------| 34 | | Input | `-M input ` | `-I ` | 35 | | Output | `-M output ` | `-O ` | 36 | | Cache | `-M cache ` | `-C ` | 37 | ~~~ 38 | 39 | For example, suppose we have a simple container (`disk-usage.rapt`) that just 40 | calculates the disk space used by a mount: 41 | 42 | ```raptor 43 | FROM docker://debian:trixie 44 | 45 | # Declare the mount "input", and place it at /input 46 | MOUNT input /input 47 | 48 | # Calculate the disk space used in /input 49 | CMD "du -sh /input" 50 | ``` 51 | 52 | If we try to run this, we will get an error message: 53 | 54 | ```sh 55 | $ sudo raptor run disk-usage 56 | [*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie 57 | [*] Completed [A24E97B01374CFEF] disk-usage 58 | [E] Raptor failed: Required mount [input] not specified 59 | ``` 60 | 61 | As we can see, the container builds correctly, but fails because the `input` 62 | mount is not specified. 63 | 64 | To fix this, we specify the input mount: 65 | 66 | ```sh 67 | sudo raptor run disk-usage -I /tmp 68 | [*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie 69 | [*] Completed [4BDD0649E00CA728] disk-usage 70 | 128M /input 71 | ``` 72 | 73 | We could have specified `-I /tmp` as `-M input /tmp`, but the short form usually 74 | makes the command easier to follow. 75 | 76 | ## Mount type 77 | 78 | The example have just looked will probably feel familiar to Docker users, since 79 | it is very similar to *docker volumes*, which are also bind mounts from the host 80 | to the container namespace. 81 | 82 | However, Raptor mounts are more advanced than that. 83 | 84 | For example, in order to build a Debian liveboot iso, we need to provide the 85 | Debian `live-boot` scripts with a set of `squashfs` files, generated 86 | from the individual layers of one or more raptor targets. 87 | 88 | Docker does not easily provide access to container layers, as it is seen as more 89 | of an implementation detail. 90 | 91 | In contrast, Raptor considers layers a first-class object, and makes them 92 | available using the `MOUNT` instruction. 93 | 94 | To learn more about different mount types, please see the [mount 95 | types](../mount-types.md) section. 96 | -------------------------------------------------------------------------------- /book/src/mount-types/layers.md: -------------------------------------------------------------------------------- 1 | ## Mount type `--layers` 2 | 3 | The `--simple` and `--file` mount types both present content from the host 4 | filesystem inside the container, and both have equivalents in Docker. 5 | 6 | This is not the case for `--layers`, which is specific to Raptor. 7 | 8 | When using a `--layers` mount, the input argument when running Raptor is not a 9 | file path, but a set of *Raptor build targets*. 10 | 11 | Let us take a look at a target, that lists the files in a `--layers` mounts: 12 | 13 | ~~~admonish note title="layers-lister.rapt" 14 | ```raptor 15 | {{#include ../../example/layers-lister.rapt}} 16 | ``` 17 | ~~~ 18 | 19 | Let's try running this, using the previous `file-lister.rapt` target as input: 20 | 21 | ```sh 22 | $ sudo raptor run layers-lister -I file-lister 23 | total 12 24 | drwxr-xr-x 2 root root 4096 Oct 17 13:37 file-lister-16689594BA5D2989 25 | drwxr-xr-x 17 root root 4096 Sep 29 00:00 index.docker.io-library-debian-trixie-675DE2C3A4D8CD82 26 | -rw-r--r-- 1 root root 200 Oct 17 13:37 raptor.json 27 | ``` 28 | 29 | We see that each layer in the input is available as a directory, but also a 30 | `raptor.json` file. 31 | 32 | Think of this file as a manifest of the contents in the `--layers` mount. It 33 | contains useful metadata about the inputs, including which targets have been 34 | specified, as well as the stacking order for each target: 35 | 36 | ~~~admonish note title="raptor.json" 37 | ```json 38 | { 39 | "targets": [ 40 | "file-lister" 41 | ], 42 | "layers": { 43 | "file-lister": [ 44 | "index.docker.io-library-debian-trixie-675DE2C3A4D8CD82", 45 | "file-lister-16689594BA5D2989" 46 | ] 47 | } 48 | } 49 | ``` 50 | ~~~ 51 | 52 | At first, this might seem like overly complicated. If we just needed the layer 53 | order, surely a simple text file would suffice? 54 | 55 | In fact, *multiple* build targets can be specified at the same time: 56 | 57 | ```sh 58 | $ sudo raptor run layers-lister -I file-lister -I file-lister-output 59 | total 16 60 | drwxr-xr-x 2 root root 4096 Oct 17 11:33 file-lister-16689594BA5D2989 61 | drwxr-xr-x 2 root root 4096 Oct 17 11:31 file-lister-output-2CFDE4FEBD507157 62 | drwxr-xr-x 17 root root 4096 Sep 29 00:00 index.docker.io-library-debian-trixie-675DE2C3A4D8CD82 63 | -rw-r--r-- 1 root root 381 Oct 17 11:45 raptor.json 64 | ``` 65 | 66 | We now have multiple build targets, and the Docker layer is shared between both 67 | inputs. 68 | 69 | However, we can still make sense of this using `raptor.json`: 70 | 71 | ~~~admonish note title="raptor.json" 72 | ```json 73 | { 74 | "targets": [ 75 | "file-lister", 76 | "file-lister-output" 77 | ], 78 | "layers": { 79 | "file-lister-output": [ 80 | "index.docker.io-library-debian-trixie-675DE2C3A4D8CD82", 81 | "file-lister-output-2CFDE4FEBD507157" 82 | ], 83 | "file-lister": [ 84 | "index.docker.io-library-debian-trixie-675DE2C3A4D8CD82", 85 | "file-lister-16689594BA5D2989" 86 | ] 87 | } 88 | } 89 | ``` 90 | ~~~ 91 | 92 | This mount type is useful for any build container that needs to work the 93 | contents of individual layers. 94 | 95 | For example, the [`deblive` builder](../builders/deblive.md) uses this mount type, in order to 96 | build Debian liveboot images. 97 | -------------------------------------------------------------------------------- /book/src/inst/cmd.md: -------------------------------------------------------------------------------- 1 | # Instruction `CMD` 2 | 3 | ~~~admonish summary 4 | ```raptor 5 | CMD [...] 6 | ``` 7 | ~~~ 8 | 9 | ```admonish important title="Build-time instruction" 10 | The `CMD` instructions only affects *running* a container, not *building* a 11 | container. 12 | ``` 13 | 14 | This instruction sets the default command for the container, which is used when 15 | running commands in it. 16 | 17 | Unlike `ENTRYPOINT`, this instruction does not have a default value. 18 | 19 | The semantics of the `ENTRYPOINT` and `CMD` instructions are heavily inspired by 20 | Docker. 21 | 22 | ~~~admonish info title="About the Docker design...",collapsible=true 23 | Many people find the Docker design of `ENTRYPOINT` and `CMD` confusing, and 24 | unneccessarily complicated. So why duplicate it in Raptor? 25 | 26 | We considered other options, but in the end, a redesign would have meant more 27 | complexity for people familiar with Docker, and the risk of confusing the 28 | semantics would increase. 29 | 30 | So we are leaning towards what is familiar to some people, even though Raptor 31 | does not have the [distinction 32 | between](https://docs.docker.com/reference/dockerfile/#shell-and-exec-form) 33 | "shell form" and "exec form" that Docker has. 34 | ~~~ 35 | 36 | When running a container, the **command** will be run through the **entrypoint**. 37 | 38 | The **command** can be specified on the command line. If no command is given, 39 | the `CMD` instruction provides the fallback value. If no `CMD` instruction is 40 | found either, the run will fail with an error. 41 | 42 | The **entrypoint** is taken from the `ENTRYPOINT` instruction, if 43 | present. Otherwise, the default value is used. 44 | 45 | ## Example 46 | 47 | As an example, let us consider a simple Raptor container: 48 | 49 | ~~~admonish file title="ping.rapt" 50 | ```raptor 51 | {{#include ../../example/ping.rapt}} 52 | ``` 53 | ~~~ 54 | 55 | This container will install the `iptutils-ping` package, containing the `ping` 56 | command. Since `/bin/ping` is set as the entrypoint, the container can be 57 | invoked to ping destinations: 58 | 59 | ```sh 60 | $ sudo raptor run -L book book/example/ '$book.ping' 1.1.1.1 61 | [*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie 62 | [*] Completed [20D13F4B24A66D8B] ping 63 | PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data. 64 | 64 bytes from 1.1.1.1: icmp_seq=1 ttl=52 time=13.9 ms 65 | 64 bytes from 1.1.1.1: icmp_seq=2 ttl=52 time=15.2 ms 66 | 64 bytes from 1.1.1.1: icmp_seq=3 ttl=52 time=11.8 ms 67 | ^C 68 | --- 1.1.1.1 ping statistics --- 69 | 3 packets transmitted, 3 received, 0% packet loss, time 2003ms 70 | rtt min/avg/max/mdev = 11.762/13.613/15.190/1.412 ms 71 | ``` 72 | 73 | However, if no arguments are provided, we fall back to `8.8.8.8` (specified with 74 | the `CMD` instruction): 75 | 76 | ```sh 77 | $ sudo raptor run -L book book/example/ '$book.ping' 78 | [*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie 79 | [*] Completed [20D13F4B24A66D8B] ping 80 | PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. 81 | 64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=9.04 ms 82 | 64 bytes from 8.8.8.8: icmp_seq=2 ttl=117 time=9.24 ms 83 | ^C 84 | --- 8.8.8.8 ping statistics --- 85 | 2 packets transmitted, 2 received, 0% packet loss, time 1000ms 86 | rtt min/avg/max/mdev = 9.039/9.137/9.235/0.098 ms 87 | ``` 88 | -------------------------------------------------------------------------------- /crates/dregistry/src/digest.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::error::{DResult, DockerError}; 6 | 7 | #[derive(Clone, Hash, PartialEq, Eq)] 8 | pub enum Digest { 9 | Sha256([u8; 32]), 10 | } 11 | 12 | impl Digest { 13 | pub fn parse(value: &str) -> DResult { 14 | let (typ, hex) = value.split_once(':').ok_or(DockerError::DigestError)?; 15 | 16 | match typ { 17 | "sha256" => { 18 | if hex.len() != 64 { 19 | return Err(DockerError::DigestError); 20 | } 21 | 22 | let mut hash = [0u8; 32]; 23 | hex::decode_to_slice(hex, &mut hash)?; 24 | 25 | Ok(Self::Sha256(hash)) 26 | } 27 | _ => Err(DockerError::DigestError), 28 | } 29 | } 30 | } 31 | 32 | impl Display for Digest { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | match self { 35 | Self::Sha256(hash) => { 36 | write!(f, "sha256:{}", hex::encode(hash)) 37 | } 38 | } 39 | } 40 | } 41 | 42 | impl Debug for Digest { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | match self { 45 | Self::Sha256(hash) => f.debug_tuple("Sha256").field(&hex::encode(hash)).finish(), 46 | } 47 | } 48 | } 49 | 50 | impl Serialize for Digest { 51 | fn serialize(&self, serializer: S) -> Result 52 | where 53 | S: serde::Serializer, 54 | { 55 | match self { 56 | Self::Sha256(hash) => { 57 | serializer.serialize_str(&format!("sha256:{}", hex::encode(hash))) 58 | } 59 | } 60 | } 61 | } 62 | 63 | impl<'de> Deserialize<'de> for Digest { 64 | fn deserialize(deserializer: D) -> Result 65 | where 66 | D: serde::Deserializer<'de>, 67 | { 68 | use serde::de::Error; 69 | 70 | let value = String::deserialize(deserializer)?; 71 | 72 | Self::parse(&value).map_err(Error::custom) 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use crate::digest::Digest; 79 | 80 | // simple pseudo-random generator, used to provide non-pathological test cases 81 | #[allow(clippy::needless_range_loop)] 82 | fn simple_rand(data: &mut [u8]) { 83 | let mut acc: u64 = 0x10001; 84 | for i in 0..data.len() { 85 | acc = acc.wrapping_mul(1337).wrapping_add(i as u64); 86 | data[i] = (acc & 0xFF) as u8; 87 | } 88 | } 89 | 90 | #[test] 91 | fn sha256_parse() { 92 | let mut data = [0; 32]; 93 | simple_rand(&mut data); 94 | 95 | let src = Digest::Sha256(data); 96 | let spec = src.to_string(); 97 | let dst = Digest::parse(&spec).unwrap(); 98 | 99 | assert_eq!(src, dst); 100 | } 101 | 102 | #[test] 103 | fn sha256_parse_roundtrip() { 104 | let mut src = [0; 32]; 105 | simple_rand(&mut src); 106 | 107 | let spec = format!("sha256:{}", hex::encode(src)); 108 | let Digest::Sha256(dst) = Digest::parse(&spec).unwrap(); 109 | 110 | assert_eq!(src, dst); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /book/src/walkthrough/debian/make.md: -------------------------------------------------------------------------------- 1 | # Make 2 | 3 | In the [previous chapter](iso.md) we took a look at the final command used to 4 | build a debian liveboot iso: 5 | 6 | ```sh 7 | sudo raptor run \ 8 | --link book book/example \ 9 | --link rbuild ../raptor-builders \ 10 | --cache liveboot-cache \ 11 | --input '$book.ssh' \ 12 | --output custom-liveboot.iso \ 13 | '$rbuild.deblive' 14 | ``` 15 | 16 | As mentioned, this works, but it doesn't exactly roll off the tongue. 17 | 18 | Having to type this command every time we want to build the iso, *is not a 19 | satisfying solution*. 20 | 21 | To solve this, the subcommand `raptor make` is used. It is a 22 | [make](https://www.gnu.org/software/make/make.html)-like system, where 23 | dependencies are specified in a configuration file. 24 | 25 | For `make`, this is a `Makefile`, for Rust it's `Cargo.toml`, and for `raptor 26 | make` we have `Raptor.toml`. 27 | 28 | Building a `Raptor.toml` for your project is recommend, but *not required*. It 29 | is perfectly possible to start using `raptor` from the command line, and only 30 | write a `Raptor.toml` file when it feels right. 31 | 32 | That being said, the format for `Raptor.toml` has been designed to be a smooth 33 | transition from a constructed command line. The smallest valid `Raptor.toml` 34 | file is an empty one. 35 | 36 | Let's start by adding the two linked packages (`--link` arguments): 37 | 38 | ~~~admonish title="Raptor.toml" 39 | ```toml 40 | [raptor.link] 41 | book = "book/example" 42 | rbuild = "../raptor-builders" 43 | ``` 44 | ~~~ 45 | 46 | We have added two package links, but of course any number can be added as desired. 47 | 48 | All sections named `[raptor.*]` are settings for Raptor. The section 49 | `[raptor.link]` is for specifying linked packages. 50 | 51 | Next, we will define a `run` job, which accounts for almost all of the remaining 52 | command line arguments. 53 | 54 | A `Raptor.toml` file can have any number of `[run.*]` sections, each with their 55 | own name. Each of those sections is a separate `run` job. Let's use `book-ssh` 56 | for this example: 57 | 58 | ~~~admonish title="Raptor.toml" 59 | ```toml 60 | [raptor.link] 61 | book = "book/example" 62 | rbuild = "../raptor-builders" 63 | 64 | # The name `book-ssh` is not special. 65 | # Feel free to choose any name you like! 66 | [run.book-ssh] 67 | target = "$rbuild.deblive" 68 | cache = "liveboot-cache" 69 | input = "$book.ssh" 70 | output = "custom-liveboot.iso" 71 | ``` 72 | ~~~ 73 | 74 | ~~~admonish example title="Click here for more details on `[raptor.link]`" collapsible=true 75 | You can choose any names for the linked packages. 76 | 77 | For example, instaed of this: 78 | 79 | ```toml 80 | [raptor.link] 81 | book = "book/example" 82 | ... 83 | 84 | [run.example-1] 85 | # $book refers to the link name above 86 | input = "$book.ssh" 87 | ... 88 | ``` 89 | 90 | ...the following `Raptor.toml` is equivalent: 91 | 92 | ```toml 93 | [raptor.link] 94 | example = "book/example" 95 | ... 96 | 97 | [run.example-2] 98 | # $example refers to the link name above 99 | input = "$example.ssh" 100 | ... 101 | ``` 102 | ~~~ 103 | 104 | 105 | Now that we have encoded all the arguments, we can use a much simpler `raptor 106 | make` command to run this build job: 107 | 108 | ```sh 109 | sudo raptor make book-ssh 110 | ``` 111 | -------------------------------------------------------------------------------- /src/program/error.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet}; 4 | 5 | use crate::RaptorResult; 6 | use raptor_parser::ast::Origin; 7 | use raptor_parser::util::Location; 8 | 9 | #[must_use] 10 | pub fn line_number_to_span(text: &str, line: usize) -> Range { 11 | let (a, b) = text 12 | .lines() 13 | .take(line) 14 | .fold((0, 0), |(_, b), l| (b, b + l.len() + 1)); 15 | 16 | a..b 17 | } 18 | 19 | #[must_use] 20 | pub fn index_to_line_remainder(text: &str, idx: usize) -> Option> { 21 | let mut size = 0; 22 | for ln in text.lines() { 23 | let line_end = size + ln.len(); 24 | if line_end > idx { 25 | return Some(idx..line_end); 26 | } 27 | size = line_end + 1; 28 | } 29 | 30 | if size >= idx { 31 | return Some(idx..size); 32 | } 33 | 34 | None 35 | } 36 | 37 | #[must_use] 38 | pub fn context_lines(src: &str, range: Range, lines: usize) -> Range { 39 | let mut a = range.start; 40 | for _ in 0..lines { 41 | if let Some(m) = src[..a].rfind('\n') { 42 | a = m; 43 | } else { 44 | break; 45 | } 46 | } 47 | 48 | let mut b = range.end; 49 | for _ in 0..lines { 50 | if let Some(m) = src[b..].find('\n') { 51 | b += m + 1; 52 | } else { 53 | break; 54 | } 55 | } 56 | 57 | a..b 58 | } 59 | 60 | pub fn show_error_context( 61 | source: &str, 62 | source_path: impl AsRef, 63 | title: &str, 64 | label: &str, 65 | err_range: Range, 66 | ) { 67 | let visible_range = context_lines(source, err_range.clone(), 3); 68 | 69 | let message = Level::ERROR.primary_title(title).element( 70 | Snippet::source(source) 71 | .fold(true) 72 | .annotation(AnnotationKind::Primary.span(err_range).label(label)) 73 | .annotation(AnnotationKind::Visible.span(visible_range)) 74 | .path(source_path.as_ref()), 75 | ); 76 | 77 | let renderer = Renderer::styled(); 78 | anstream::eprintln!("{}", renderer.render(&[message])); 79 | } 80 | 81 | pub fn show_origin_error_context(source: &str, origin: &Origin, title: &str, label: &str) { 82 | show_error_context( 83 | source, 84 | origin.path.as_ref(), 85 | title, 86 | label, 87 | origin.span.clone(), 88 | ); 89 | } 90 | 91 | pub fn show_jinja_error_context(err: &minijinja::Error) -> RaptorResult<()> { 92 | let source_path = err.name().unwrap(); 93 | let raw = std::fs::read_to_string(source_path)?; 94 | 95 | let kind_desc = err.kind().to_string(); 96 | let title = err.detail().unwrap_or(&kind_desc); 97 | let label = format!("{err}"); 98 | 99 | let err_range = err 100 | .range() 101 | .or_else(|| err.line().map(|line| line_number_to_span(&raw, line))) 102 | .unwrap_or(0..raw.len() - 1); 103 | 104 | show_error_context(&raw, source_path, title, &label, err_range); 105 | Ok(()) 106 | } 107 | 108 | pub fn show_parse_error_context( 109 | source: &str, 110 | err: &Location, 111 | ) -> RaptorResult<()> { 112 | let source_path = err.origin.path.as_str(); 113 | let title = "Parse error"; 114 | let label = err.to_string(); 115 | let err_range = err.origin.span.clone(); 116 | 117 | show_error_context(source, source_path, title, &label, err_range); 118 | 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Raptor headline logo](book/src/images/logo-title.png) 2 | 3 | # Raptor 4 | 5 | Raptor is a modern, fast, and easy-to-use system for building disk images, 6 | bootable isos, containers and much more - all from a simple, Dockerfile-inspired 7 | syntax. 8 | 9 | It uses `systemd-nspawn` for sandboxing when building or running containers. 10 | 11 | > [!TIP] 12 | > 📕 For more information, [read the raptor book](https://chrivers.github.io/raptor/) 13 | 14 | ## What it looks like 15 | 16 | Raptor uses a syntax similar to `Dockerfile`. Statements start with uppercase 17 | keywords, and are terminated by end of line. 18 | 19 | All lines starting with `#` are treated as comments: 20 | 21 | ```Dockerfile 22 | # Start from a well-known docker image 23 | FROM docker://debian:trixie 24 | 25 | # Set the hostname 26 | WRITE "example-host\n" /etc/hostname 27 | 28 | # Create app directory 29 | MKDIR -p /app/bin 30 | 31 | # This copies "program" from the host to "/app/bin" inside the build target 32 | COPY program /app/bin/program 33 | ``` 34 | 35 | ## What it can do 36 | 37 | > [!TIP] 38 | > 📕 For more information, [read the raptor book](https://chrivers.github.io/raptor/) 39 | 40 | Raptor builds *layers*, much in the same way as Docker. 41 | 42 | However, this is where the similarities end! Raptor is able to run build 43 | processes on top of finished layers, to produce any kind of desired output. 44 | 45 | The companion project [raptor-builders](https://github.com/chrivers/raptor-builders) can create: 46 | 47 | - Debian Live Boot iso files 48 | - Disk images for virtual (or physical) machines 49 | 50 | ## Example: Building a bootable iso 51 | 52 | After [installing Raptor](https://chrivers.github.io/raptor/install.html), create a file called `base.rapt`: 53 | 54 | ```Dockerfile 55 | # Start from a docker iso 56 | FROM docker://debian:trixie 57 | 58 | # Set root password to "raptor" 59 | RUN usermod -p "$1$GQf2tS9s$vu72NbrDtUcvvqnyAogrH0" root 60 | 61 | # Update package sources, and install packages 62 | RUN apt-get update 63 | RUN apt-get install -qy systemd-sysv live-boot linux-image-amd64 64 | ``` 65 | 66 | Then clone the `raptor-builders` project, which has the build container for making Debian Live Boot images: 67 | 68 | ```sh 69 | git clone https://github.com/chrivers/raptor-builders.git 70 | ``` 71 | 72 | Then run the `deblive` container from `raptor-builders`, using the `base(.rapt)` we just made: 73 | 74 | ```sh 75 | # Create cache dir (used in `-C` option) 76 | mkdir /tmp/raptor-cache 77 | 78 | # Run the `deblive` builder from `raptor-builders` 79 | sudo raptor run \ 80 | '$rbuild.deblive' \ 81 | -L rbuild raptor-builders \ 82 | -C /tmp/raptor-cache \ 83 | -I base \ 84 | -O liveboot.iso 85 | ``` 86 | 87 | After this step, the file `liveboot.iso` is ready to use. We can try it out with QEMU: 88 | 89 | ```sh 90 | qemu-system-x86_64 -enable-kvm -cpu host -m 4G -cdrom liveboot.iso 91 | ``` 92 | 93 | > [!TIP] 94 | > 📕 The whole process is described in [much more detail in the book!](https://chrivers.github.io/raptor/walkthrough/debian/). 95 | 96 | ## Need help? 97 | 98 | The 📕 [Raptor Book](https://chrivers.github.io/raptor/) contains a lot more 99 | information, including a thorough description of all instructions, features, and 100 | a grammar for the language itself. 101 | 102 | ## License 103 | 104 | Raptor is Free Software, licensed under the GNU GPL-3.0. 105 | 106 | The [Raptor icon](book/branding/raptor-logo.svg) is derived from a [Creative Commons Attribution](https://www.freepik.com/icon/eagle_17553500)-licensed icon. 107 | -------------------------------------------------------------------------------- /src/dsl/program.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | use camino::Utf8PathBuf; 4 | use colored::Colorize; 5 | use minijinja::Value; 6 | 7 | use raptor_parser::ast::{ 8 | FromSource, InstCmd, InstEntrypoint, InstMount, Instruction, Origin, Statement, 9 | }; 10 | 11 | use crate::RaptorResult; 12 | use crate::dsl::Item; 13 | 14 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 15 | pub struct Program { 16 | pub code: Vec, 17 | pub ctx: Value, 18 | pub path: Utf8PathBuf, 19 | } 20 | 21 | impl Program { 22 | #[must_use] 23 | pub const fn new(code: Vec, ctx: Value, path: Utf8PathBuf) -> Self { 24 | Self { code, ctx, path } 25 | } 26 | 27 | pub fn traverse( 28 | &self, 29 | visitor: &mut impl FnMut(&Statement) -> RaptorResult<()>, 30 | ) -> RaptorResult<()> { 31 | for stmt in &self.code { 32 | match stmt { 33 | Item::Statement(stmt) => visitor(stmt)?, 34 | Item::Program(prog) => { 35 | prog.traverse(visitor)?; 36 | } 37 | } 38 | } 39 | Ok(()) 40 | } 41 | 42 | #[must_use] 43 | pub fn from(&self) -> Option<(&FromSource, &Origin)> { 44 | for item in &self.code { 45 | if let Item::Statement(Statement { 46 | inst: Instruction::From(inst), 47 | origin, 48 | }) = item 49 | { 50 | return Some((&inst.from, origin)); 51 | } 52 | } 53 | 54 | None 55 | } 56 | 57 | #[must_use] 58 | pub fn cmd(&self) -> Option<&InstCmd> { 59 | for item in &self.code { 60 | if let Item::Statement(Statement { 61 | inst: Instruction::Cmd(inst), 62 | .. 63 | }) = item 64 | { 65 | return Some(inst); 66 | } 67 | } 68 | 69 | None 70 | } 71 | 72 | #[must_use] 73 | pub fn entrypoint(&self) -> Option<&InstEntrypoint> { 74 | for item in &self.code { 75 | if let Item::Statement(Statement { 76 | inst: Instruction::Entrypoint(inst), 77 | .. 78 | }) = item 79 | { 80 | return Some(inst); 81 | } 82 | } 83 | 84 | None 85 | } 86 | 87 | #[must_use] 88 | pub fn mounts(&self) -> Vec<&InstMount> { 89 | let mut mounts = vec![]; 90 | 91 | for item in &self.code { 92 | if let Item::Statement(Statement { 93 | inst: Instruction::Mount(inst), 94 | .. 95 | }) = item 96 | { 97 | mounts.push(inst); 98 | } 99 | } 100 | 101 | mounts 102 | } 103 | } 104 | 105 | impl Display for Program { 106 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 107 | fn dump(f: &mut fmt::Formatter, program: &Program, level: usize) -> fmt::Result { 108 | let indent = if f.alternate() { 109 | &" ".repeat(level * 4) 110 | } else { 111 | "" 112 | }; 113 | writeln!(f, "{indent}{}{}", "# file ".dimmed(), program.path)?; 114 | for item in &program.code { 115 | match item { 116 | Item::Statement(stmt) => { 117 | writeln!(f, "{indent}{}", stmt.inst)?; 118 | } 119 | Item::Program(prog) => { 120 | dump(f, prog, level + 1)?; 121 | } 122 | } 123 | } 124 | Ok(()) 125 | } 126 | 127 | dump(f, self, 0) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /book/src/builders/index.md: -------------------------------------------------------------------------------- 1 | # Raptor Builders 2 | 3 | After carefully building a set of layers with Raptor, they are ready to process 4 | into more useful build artifacts. 5 | 6 | The way this is done in Raptor, is through *build containers*. These are, 7 | themselves, Raptor containers, made specifically for producing a certain kind of 8 | output. 9 | 10 | Raptor itself does not come with any build containers, but the *companion 11 | project*, [raptor-builders](https://github.com/chrivers/raptor-builders) 12 | does. It's called the "companion" project to highlight the fact that it is not 13 | above other Raptor projects. Anyone can make build containers, and if you need 14 | to build something that is uncommon, or specialized to a niche situtation, you 15 | might have to. 16 | 17 | Most people, however, should be able to get started quickly, and save a lot of 18 | time, by using the build containers from 19 | [raptor-builders](https://github.com/chrivers/raptor-builders). 20 | 21 | ## Meet the builders 22 | 23 | | Builder | Output | Supported output formats | 24 | |-----------------------------------------|------------------------------|-----------------------------| 25 | | [`deblive`](deblive.md) | Debian Liveboot iso | `iso` | 26 | | [`live-disk-image`](live-disk-image.md) | Debian Liveboot disk image | `raw`, `qcow2`, `vmdk`, ... | 27 | | [`disk-image`](disk-image.md) | Disk image | `raw`, `qcow2`, `vmdk`, ... | 28 | | [`part-image`](part-image.md) | Partition (filesystem) image | `raw`, `qcow2`, `vmdk`, ... | 29 | | [`docker-image`](docker-image.md) | Docker image | `tar` | 30 | 31 | ## Compatibility 32 | 33 | The various builders can construct a wide variety of outputs, suitable for use 34 | with both containers (`systemd-nspawn`), virtual machines (e.g. `qemu`), and 35 | physical hardware. 36 | 37 | However, not all combinations are possible. For example, a physical machine will 38 | not boot a `qcow2` image for virtual machines, but `qemu` will be able to boot 39 | either `qcow2` or `raw` images. 40 | 41 | The tables below provides an overview of the possible options. 42 | 43 | Machines: 44 | 45 | | Builder | Format | Virtual Machine | Physical Machine | 46 | |:------------------|---------|:-------------------|:-------------------| 47 | | `deblive` | `iso` | UEFI:✅ -- BIOS:✅ | UEFI:✅ -- BIOS:✅ | 48 | | `live-disk-image` | `qcow2` | UEFI:✅ -- BIOS:❌ | UEFI:❌ -- BIOS:❌ | 49 | | `disk-image` | `qcow2` | UEFI:✅ -- BIOS:❌ | UEFI:❌ -- BIOS:❌ | 50 | | `live-disk-image` | `raw` | UEFI:✅ -- BIOS:❌ | UEFI:✅ -- BIOS:❌ | 51 | | `disk-image` | `raw` | UEFI:✅ -- BIOS:❌ | UEFI:✅ -- BIOS:❌ | 52 | | `part-image` | `raw` | UEFI:❌ -- BIOS:❌ | UEFI:❌ -- BIOS:❌ | 53 | | `docker-image` | `tar` | UEFI:❌ -- BIOS:❌ | UEFI:❌ -- BIOS:❌ | 54 | 55 | ~~~admonish note 56 | Currently, booting in BIOS mode is only supported by the `deblive` builder, but 57 | the `live-disk-image` and `disk-image` builders could possibly be extended to 58 | support this, in the future. 59 | ~~~ 60 | 61 | Containers: 62 | 63 | | Builder | Format | `systemd-nspawn` | `docker` | `podman` | 64 | |:------------------|---------|:-----------------|:---------|----------| 65 | | `deblive` | `iso` | ❌ | ❌ | ❌ | 66 | | `live-disk-image` | `qcow2` | ❌ | ❌ | ❌ | 67 | | `disk-image` | `qcow2` | ❌ | ❌ | ❌ | 68 | | `live-disk-image` | `raw` | ❌ | ❌ | ❌ | 69 | | `disk-image` | `raw` | ✅ | ❌ | ❌ | 70 | | `part-image` | `raw` | ✅ | ❌ | ❌ | 71 | | `docker-image` | `tar` | ❌ | ✅ | ✅ | 72 | -------------------------------------------------------------------------------- /book/src/grammar.md: -------------------------------------------------------------------------------- 1 | # Grammar 2 | 3 | ~~~admonish warning 4 | The Raptor parser is implemented by hand in a 5 | [separate crate](https://github.com/chrivers/raptor/tree/master/crates/raptor-parser). 6 | 7 | Because it is hand-written, there is no exact BNF grammar that matches the parsing. 8 | 9 | The following is an attempt to provide a detailed and realistic description of 10 | the accepted syntax, but might contain minor mistakes and inconsistencies. 11 | ~~~ 12 | 13 | The parsing starts from the first rule, ``, and proceeds from there. 14 | 15 | ~~~admonish tip title="Syntax guide" 16 | | Syntax | Meaning | 17 | |:---------------------|:-----------------------------------------------------------------| 18 | | `rule?` | Match `rule` 0 or 1 times (i.e., it is optional) | 19 | | `rule+` | Match `rule` 1 or more times | 20 | | `rule*` | Match `rule` 0 or more times | 21 | | `rule1 \\| rule2` | Match either `rule1` or `rule2` (exactly one of them must match) | 22 | | `( rule1 rule2 .. )` | Parenthesized rules are matched/optional/repeated together | 23 | | `"word"` | Matches the letters `w`, `o`, `r`, `d` (but not the quotes) | 24 | ~~~ 25 | 26 | ~~~admonish summary title="Raptor grammar" 27 | ```bnf 28 | ::= * 29 | 30 | ::= 31 | | 32 | | 33 | | 34 | | 35 | | 36 | | 37 | | 38 | | 39 | | 40 | | 41 | | 42 | 43 | ::= "FROM" "\n" 44 | ::= "MOUNT" ? "\n" 45 | ::= "RENDER" * * "\n" 46 | ::= "WRITE" * "\n" 47 | ::= "MKDIR" * "\n" 48 | ::= "COPY" * + "\n" 49 | ::= "INCLUDE" * "\n" 50 | ::= "RUN" + "\n" 51 | ::= "ENV" + "\n" 52 | ::= "WORKDIR" "\n" 53 | ::= "ENTRYPOINT" * "\n" 54 | ::= "CMD" * "\n" 55 | 56 | ::= ( "=" )? 57 | ::= "--file" | "--simple" | "--layers" | "--overlay" 58 | 59 | ::= | "-p" 60 | ::= | 61 | ::= "--chown" "="? 62 | ::= "--chmod" "="? 63 | ::= ( (":" ?)?) | (":" ?) 64 | ::= /* built-in rule: 3 or 4 octal digits */ 65 | 66 | ::= ( "=" )? 67 | ::= | 68 | ::= ("." )* 69 | ::= 70 | | 71 | | 72 | | 73 | | 74 | ::= "[" ( ( "," )* ","? )? "]" 75 | ::= "{" ( ( "," )* ","? )? "}" 76 | ::= ":" 77 | ::= /* built-in rule: see section on string escapes */ 78 | ::= + 79 | ::= "true" | "false" 80 | ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" 81 | 82 | ::= ? ? 83 | ::= "$" ? "." 84 | ::= ( "." )* 85 | ::= "@" 86 | ``` 87 | ~~~ 88 | -------------------------------------------------------------------------------- /src/util/capture_proc_fd.rs: -------------------------------------------------------------------------------- 1 | use std::os::fd::{AsFd, IntoRawFd, OwnedFd}; 2 | use std::os::unix::process::CommandExt; 3 | use std::process::Command; 4 | 5 | /// Command extension to hook a specific file descriptor before executing 6 | /// process, using `nix::unistd::dup2()` 7 | pub trait HookFd { 8 | fn hook_fd(&mut self, fd: i32, dst: OwnedFd) -> &mut Self; 9 | } 10 | 11 | impl HookFd for Command { 12 | fn hook_fd(&mut self, fd: i32, dst: OwnedFd) -> &mut Self { 13 | unsafe { 14 | self.pre_exec(move || { 15 | // Call .into_raw_fd() to unwrap the returned file handle, so we 16 | // don't immediately drop the dup'ed file handle. 17 | let _ = nix::unistd::dup2_raw(dst.as_fd(), fd)?.into_raw_fd(); 18 | 19 | Ok(()) 20 | }) 21 | } 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use std::fs::File; 28 | use std::io::Read; 29 | use std::process::{Command, Stdio}; 30 | 31 | use nix::fcntl::OFlag; 32 | 33 | use crate::RaptorResult; 34 | use crate::util::capture_proc_fd::HookFd; 35 | 36 | #[test] 37 | fn test_hook_stdout() -> RaptorResult<()> { 38 | let (read, write) = nix::unistd::pipe2(OFlag::O_CLOEXEC)?; 39 | 40 | let mut proc = Command::new("/bin/sh") 41 | .arg("-c") 42 | .arg("echo 1 > /dev/stdout; echo 2 > /dev/stderr") 43 | .hook_fd(1, write) 44 | .stderr(Stdio::piped()) 45 | .spawn()?; 46 | 47 | let mut output = String::new(); 48 | 49 | let mut stderr = proc.stderr.take().unwrap(); 50 | let mut reader: File = read.into(); 51 | 52 | reader.read_to_string(&mut output)?; 53 | assert_eq!(output, "1\n"); 54 | 55 | output.clear(); 56 | 57 | stderr.read_to_string(&mut output)?; 58 | assert_eq!(output, "2\n"); 59 | 60 | proc.wait()?; 61 | 62 | Ok(()) 63 | } 64 | 65 | #[test] 66 | fn test_hook_stderr() -> RaptorResult<()> { 67 | let (read, write) = nix::unistd::pipe2(OFlag::O_CLOEXEC)?; 68 | 69 | let mut proc = Command::new("/bin/sh") 70 | .arg("-c") 71 | .arg("echo 1 > /dev/stdout; echo 2 > /dev/stderr") 72 | .stdout(Stdio::piped()) 73 | .hook_fd(2, write) 74 | .spawn()?; 75 | 76 | let mut output = String::new(); 77 | let mut stdout = proc.stdout.take().unwrap(); 78 | 79 | stdout.read_to_string(&mut output)?; 80 | assert_eq!(output, "1\n"); 81 | 82 | output.clear(); 83 | 84 | let mut reader: File = read.into(); 85 | reader.read_to_string(&mut output)?; 86 | assert_eq!(output, "2\n"); 87 | 88 | proc.wait()?; 89 | 90 | Ok(()) 91 | } 92 | 93 | #[test] 94 | fn test_hook_fd_3() -> RaptorResult<()> { 95 | let (read, write) = nix::unistd::pipe2(OFlag::O_CLOEXEC)?; 96 | 97 | let mut proc = Command::new("/bin/sh") 98 | .arg("-c") 99 | .arg("echo 1 > /dev/stdout; echo 2 > /dev/stderr; echo 3 >&3") 100 | .hook_fd(3, write) 101 | .stdout(Stdio::piped()) 102 | .stderr(Stdio::piped()) 103 | .spawn()?; 104 | 105 | let mut output = String::new(); 106 | 107 | let mut stdout = proc.stdout.take().unwrap(); 108 | let mut stderr = proc.stderr.take().unwrap(); 109 | let mut reader: File = read.into(); 110 | 111 | stdout.read_to_string(&mut output)?; 112 | assert_eq!(output, "1\n"); 113 | 114 | output.clear(); 115 | 116 | stderr.read_to_string(&mut output)?; 117 | assert_eq!(output, "2\n"); 118 | 119 | output.clear(); 120 | 121 | reader.read_to_string(&mut output)?; 122 | assert_eq!(output, "3\n"); 123 | 124 | proc.wait()?; 125 | 126 | Ok(()) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /crates/dregistry/src/reference.rs: -------------------------------------------------------------------------------- 1 | use pest_consume::{Parser, match_nodes}; 2 | 3 | use crate::digest::Digest; 4 | use crate::error::DResult; 5 | use crate::source::DockerSource; 6 | 7 | #[derive(pest_consume::Parser)] 8 | #[grammar = "reference.pest"] 9 | pub struct DockerTagParser; 10 | 11 | #[derive(Default)] 12 | struct Prefix { 13 | host: Option, 14 | port: Option, 15 | } 16 | 17 | type Result = std::result::Result>; 18 | pub type Node<'i> = pest_consume::Node<'i, Rule, ()>; 19 | 20 | #[allow(non_snake_case, clippy::unnecessary_wraps)] 21 | #[pest_consume::parser] 22 | impl DockerTagParser { 23 | fn hostname(input: Node) -> Result { 24 | Ok(input.as_str().to_string()) 25 | } 26 | 27 | fn port(input: Node) -> Result { 28 | Ok(input.as_str().parse().map_err(|e| input.error(e))?) 29 | } 30 | 31 | fn name_component(input: Node) -> Result { 32 | Ok(input.as_str().to_string()) 33 | } 34 | 35 | fn name_components(input: Node) -> Result { 36 | Ok(input.as_str().to_string()) 37 | } 38 | 39 | fn prefix(input: Node) -> Result { 40 | match_nodes!( 41 | input.into_children(); 42 | [] => Ok(Prefix { host: None, port: None }), 43 | [hostname(host)] => Ok(Prefix { host: Some(host), port: None }), 44 | [hostname(host), port(port)] => Ok(Prefix { host: Some(host), port: Some(port) }), 45 | ) 46 | } 47 | 48 | fn image_name(input: Node) -> Result<(Prefix, Option, String)> { 49 | let (prefix, names) = match_nodes!( 50 | input.into_children(); 51 | [name_components(names)] => (Prefix::default(), names), 52 | [prefix(pf), name_components(names)] => (pf, names), 53 | ); 54 | 55 | if let Some((head, tail)) = names.rsplit_once('/') { 56 | Ok((prefix, Some(head.to_string()), tail.to_string())) 57 | } else { 58 | Ok((prefix, None, names)) 59 | } 60 | } 61 | 62 | fn tag_name(input: Node) -> Result { 63 | Ok(input.as_str().to_string()) 64 | } 65 | 66 | fn digest(input: Node) -> Result { 67 | Ok(Digest::parse(input.as_str()).map_err(|e| input.error(e))?) 68 | } 69 | 70 | fn container(input: Node) -> Result { 71 | match_nodes!( 72 | input.into_children(); 73 | 74 | [image_name((prefix, namespace, repository))] => Ok(DockerSource { 75 | host: prefix.host, 76 | port: prefix.port, 77 | namespace, 78 | repository, 79 | tag: None, 80 | digest: None, 81 | }), 82 | 83 | [image_name((prefix, namespace, repository)), tag_name(tn)] => Ok(DockerSource { 84 | host: prefix.host, 85 | port: prefix.port, 86 | namespace, 87 | repository, 88 | tag: Some(tn), 89 | digest: None, 90 | }), 91 | 92 | [image_name((prefix, namespace, repository)), digest(dg)] => Ok(DockerSource { 93 | host: prefix.host, 94 | port: prefix.port, 95 | namespace, 96 | repository, 97 | tag: None, 98 | digest: Some(dg), 99 | }), 100 | ) 101 | } 102 | 103 | fn EOI(input: Node) -> Result<()> { 104 | Ok(()) 105 | } 106 | 107 | fn DOCKER_REFERENCE(input: Node) -> Result { 108 | match_nodes!( 109 | input.into_children(); 110 | [container(ctr), _EOI] => Ok(ctr) 111 | ) 112 | } 113 | } 114 | 115 | pub fn parse(input: &str) -> DResult { 116 | let inputs = DockerTagParser::parse(Rule::DOCKER_REFERENCE, input)?; 117 | let input = inputs.single()?; 118 | Ok(DockerTagParser::DOCKER_REFERENCE(input)?) 119 | } 120 | -------------------------------------------------------------------------------- /src/tui/joblist.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use dep_graph::DepGraph; 4 | use ratatui::buffer::Buffer; 5 | use ratatui::layout::Rect; 6 | use ratatui::style::{Color, Modifier, Style}; 7 | use ratatui::text::{Line, Span}; 8 | use ratatui::widgets::{Block, Borders, Padding, Paragraph, StatefulWidget, Widget}; 9 | 10 | use crate::batch::JobController; 11 | use crate::make::planner::{Job, Planner}; 12 | use crate::tui::jobstate::JobState; 13 | use crate::tui::ptyctrl::PtyJobController; 14 | 15 | pub struct JobList { 16 | jobs: Vec<(usize, u64)>, 17 | targetlist: HashMap, 18 | } 19 | 20 | #[derive(Default)] 21 | pub struct JobStats { 22 | pub planned: usize, 23 | pub running: usize, 24 | pub completed: usize, 25 | pub failed: usize, 26 | } 27 | 28 | impl JobStats { 29 | #[must_use] 30 | pub const fn sum(&self) -> usize { 31 | self.planned + self.running + self.completed + self.failed 32 | } 33 | 34 | #[must_use] 35 | pub const fn complete(&self) -> bool { 36 | (self.planned + self.running) == 0 37 | } 38 | } 39 | 40 | impl JobList { 41 | #[must_use] 42 | pub fn new(planner: Planner) -> Self { 43 | let (plan, targetlist) = planner.into_plan(); 44 | 45 | let mut jobs = vec![]; 46 | for node in &plan.ready_nodes { 47 | Self::generate_sublist(&plan, *node, 1, &mut jobs); 48 | } 49 | 50 | Self { jobs, targetlist } 51 | } 52 | 53 | #[must_use] 54 | pub const fn lines(&self) -> usize { 55 | self.jobs.len() 56 | } 57 | 58 | #[must_use] 59 | pub fn complete(&self, ctrl: &PtyJobController) -> bool { 60 | self.targetlist 61 | .keys() 62 | .all(|id| ctrl.job_state(*id) == JobState::Completed) 63 | } 64 | 65 | #[must_use] 66 | pub fn stats(&self, ctrl: &impl JobController) -> JobStats { 67 | let mut stats = JobStats::default(); 68 | for key in self.targetlist.keys() { 69 | match ctrl.job_state(*key) { 70 | JobState::Planned => stats.planned += 1, 71 | JobState::Running => stats.running += 1, 72 | JobState::Completed => stats.completed += 1, 73 | JobState::Failed => stats.failed += 1, 74 | } 75 | } 76 | stats 77 | } 78 | 79 | fn generate_sublist( 80 | plan: &DepGraph, 81 | node: u64, 82 | indent: usize, 83 | list: &mut Vec<(usize, u64)>, 84 | ) { 85 | list.push((indent, node)); 86 | 87 | let Ok(read) = plan.rdeps.read() else { 88 | return; 89 | }; 90 | 91 | for node in read.get(&node).into_iter().flatten() { 92 | Self::generate_sublist(plan, *node, indent + 1, list); 93 | } 94 | } 95 | } 96 | 97 | pub struct JobView<'a> { 98 | pub list: &'a JobList, 99 | pub ctrl: &'a PtyJobController, 100 | } 101 | 102 | impl<'a> JobView<'a> { 103 | #[must_use] 104 | pub const fn new(list: &'a JobList, ctrl: &'a PtyJobController) -> Self { 105 | Self { list, ctrl } 106 | } 107 | } 108 | 109 | impl StatefulWidget for JobView<'_> { 110 | type State = usize; 111 | 112 | fn render(self, area: Rect, buf: &mut Buffer, index: &mut Self::State) { 113 | let mut lines = vec![]; 114 | for (indent, id) in &self.list.jobs { 115 | let state = self.ctrl.job_state(*id); 116 | let mut line = Line::raw(" ".repeat(*indent)); 117 | line.push_span(Span::styled(state.symbol(*index), state.color())); 118 | line.push_span(format!(" {}", &self.list.targetlist[id])); 119 | lines.push(line); 120 | } 121 | 122 | let block = Block::default() 123 | .padding(Padding::proportional(1)) 124 | .borders(Borders::ALL) 125 | .style(Style::new().add_modifier(Modifier::BOLD).bg(Color::Black)); 126 | let p = Paragraph::new(lines).block(block); 127 | 128 | p.render(area, buf); 129 | 130 | *index += 1; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/template/args.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use clap::{ArgMatches, Command, Id}; 4 | use minijinja::value::{Enumerator, Kwargs, Object, ObjectRepr, from_args}; 5 | use minijinja::{Environment, Error, ErrorKind, State, Value}; 6 | 7 | use serde::de::Error as _; 8 | 9 | use crate::util::kwargs::KwargsExt; 10 | 11 | #[derive(Debug)] 12 | struct Arg { 13 | name: String, 14 | required: bool, 15 | help: Option, 16 | default: Option, 17 | } 18 | 19 | #[derive(Debug)] 20 | struct Args(Mutex>); 21 | 22 | #[derive(Debug)] 23 | struct MatchWrapper(ArgMatches); 24 | 25 | impl Object for MatchWrapper { 26 | fn repr(self: &Arc) -> ObjectRepr { 27 | ObjectRepr::Map 28 | } 29 | 30 | fn get_value(self: &Arc, key: &Value) -> Option { 31 | self.0 32 | .get_one::<&str>(&key.to_string()) 33 | .map(Value::from_serialize) 34 | } 35 | 36 | fn enumerate(self: &Arc) -> Enumerator { 37 | Enumerator::Values( 38 | self.0 39 | .ids() 40 | .map(Id::as_str) 41 | .map(Value::from) 42 | .collect::>(), 43 | ) 44 | } 45 | 46 | fn enumerator_len(self: &Arc) -> Option { 47 | None 48 | } 49 | } 50 | 51 | impl Object for Args { 52 | fn call_method( 53 | self: &Arc, 54 | _state: &State, 55 | method: &str, 56 | args: &[Value], 57 | ) -> Result { 58 | let (args, kwargs) = from_args::<(&[Value], Kwargs)>(args)?; 59 | match method { 60 | "param" => { 61 | let required = kwargs.get_or_default("required", false)?; 62 | let help: Option = kwargs.get_option("help")?; 63 | let default: Option = kwargs.get_option("default")?; 64 | 65 | let (name,): (String,) = from_args(args)?; 66 | 67 | kwargs.assert_all_used()?; 68 | 69 | self.0.lock().unwrap().push(Arg { 70 | name, 71 | required, 72 | help, 73 | default, 74 | }); 75 | 76 | Ok(Value::UNDEFINED) 77 | } 78 | 79 | "parse" => { 80 | let mut cmd = Command::new("foo") 81 | .no_binary_name(true) 82 | .disable_help_flag(true) 83 | .disable_colored_help(true); 84 | 85 | let lock = self.0.lock().unwrap(); 86 | for arg in lock.iter() { 87 | let mut a = clap::Arg::new(&arg.name).required(arg.required); 88 | if let Some(h) = &arg.help { 89 | a = a.help(h); 90 | } 91 | if let Some(default) = &arg.default { 92 | a = a.default_value(default.to_string()); 93 | } 94 | cmd = cmd.arg(a); 95 | } 96 | 97 | let (value,): (Value,) = from_args(args)?; 98 | 99 | let res = cmd 100 | .try_get_matches_from(value.try_iter()?.map(|x| x.to_string())) 101 | .map_err(|e| Error::custom(format!("\n{e}")))?; 102 | 103 | drop(lock); 104 | 105 | Ok(Value::from_object(MatchWrapper(res))) 106 | } 107 | _ => todo!(), 108 | } 109 | } 110 | } 111 | 112 | #[allow(clippy::unnecessary_wraps)] 113 | pub fn args() -> Result { 114 | Ok(Value::from_object(Args(Mutex::new(vec![])))) 115 | } 116 | 117 | #[allow(clippy::unnecessary_wraps)] 118 | pub fn require(state: &State, name: &str) -> Result<(), Error> { 119 | if state.lookup(name).is_some() { 120 | Ok(()) 121 | } else { 122 | let msg = format!("Missing field {name:?}"); 123 | Err(Error::new(ErrorKind::MissingArgument, msg)) 124 | } 125 | } 126 | 127 | pub fn add_functions(env: &mut Environment) { 128 | env.add_function("Args", args); 129 | env.add_function("require", require); 130 | } 131 | -------------------------------------------------------------------------------- /src/program/executor.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use camino::Utf8PathBuf; 4 | use indicatif::{ProgressBar, ProgressStyle}; 5 | use minijinja::Value; 6 | 7 | use crate::dsl::Program; 8 | use crate::program::{Loader, ResolveArgs}; 9 | use crate::sandbox::{Sandbox, SandboxExt}; 10 | use crate::util::io_fast_copy; 11 | use crate::{RaptorResult, template}; 12 | use raptor_parser::ast::{Instruction, Statement}; 13 | 14 | pub struct Executor { 15 | sandbox: Sandbox, 16 | } 17 | 18 | impl Executor { 19 | const PROGRESS_STYLE: &str = "[{elapsed_precise}] {bar:40.cyan/blue} {bytes:>7}/{total_bytes:7} {binary_bytes_per_sec} {msg}"; 20 | 21 | #[must_use] 22 | pub const fn new(sandbox: Sandbox) -> Self { 23 | Self { sandbox } 24 | } 25 | 26 | fn progress_bar(len: u64) -> ProgressBar { 27 | let style = ProgressStyle::with_template(Self::PROGRESS_STYLE) 28 | .unwrap() 29 | .progress_chars("#>-"); 30 | 31 | ProgressBar::new(len).with_style(style) 32 | } 33 | 34 | fn handle(&mut self, stmt: &Statement, ctx: &Value) -> RaptorResult<()> { 35 | let client = self.sandbox.client(); 36 | match &stmt.inst { 37 | // Code merging and mount instruction have nothing to execute 38 | Instruction::From(_) 39 | | Instruction::Include(_) 40 | | Instruction::Mount(_) 41 | | Instruction::Entrypoint(_) 42 | | Instruction::Cmd(_) => {} 43 | 44 | Instruction::Copy(inst) => { 45 | let srcname = stmt.origin.path_for(&inst.srcs[0])?; 46 | let src = File::open(&srcname)?; 47 | let fd = client.create_file( 48 | &Utf8PathBuf::from(&inst.dest), 49 | inst.chown.clone(), 50 | inst.chmod, 51 | )?; 52 | 53 | let pb = Self::progress_bar(src.metadata()?.len()); 54 | let dst = pb.wrap_write(fd); 55 | io_fast_copy(src, dst)?; 56 | } 57 | 58 | Instruction::Render(inst) => { 59 | let map = ctx.resolve_args(&inst.args)?; 60 | 61 | let srcname = stmt.origin.path_for(&inst.src)?; 62 | 63 | let source = template::make_environment()? 64 | .get_template(srcname.as_str()) 65 | .and_then(|tmpl| tmpl.render(Value::from(map))) 66 | .map(|src| src + "\n")?; 67 | 68 | client.write_file( 69 | &inst.dest, 70 | inst.chown.clone(), 71 | inst.chmod, 72 | source.as_bytes(), 73 | )?; 74 | } 75 | 76 | Instruction::Write(inst) => { 77 | client.write_file( 78 | &inst.dest, 79 | inst.chown.clone(), 80 | inst.chmod, 81 | inst.body.as_bytes(), 82 | )?; 83 | } 84 | 85 | Instruction::Mkdir(inst) => { 86 | client.mkdir(&inst.dest, inst.chown.clone(), inst.chmod, inst.parents)?; 87 | } 88 | 89 | Instruction::Run(inst) => { 90 | client.run(&inst.run)?; 91 | } 92 | 93 | Instruction::Env(inst) => { 94 | for env in &inst.env { 95 | client.setenv(&env.key, &env.value)?; 96 | } 97 | } 98 | 99 | Instruction::Workdir(inst) => { 100 | client.chdir(inst.dir.as_str())?; 101 | } 102 | } 103 | 104 | Ok(()) 105 | } 106 | 107 | pub fn run(&mut self, loader: &Loader, program: &Program) -> RaptorResult<()> { 108 | program.traverse(&mut |stmt| { 109 | info!("{}", stmt.inst); 110 | self.handle(stmt, &program.ctx).or_else(|err| { 111 | loader.explain_error(&err, std::slice::from_ref(&stmt.origin))?; 112 | Err(err) 113 | }) 114 | }) 115 | } 116 | 117 | pub fn finish(mut self) -> RaptorResult<()> { 118 | self.sandbox.close() 119 | } 120 | } 121 | --------------------------------------------------------------------------------