Skip to content

Commit f68df95

Browse files
Jack/add build command (#1)
* WIP: added dockerfile and build command * WIP:templating * WIP: Dockerfile generates, but we need to refactor * WIP: progress: * working, temporarily using a dev branch of server * working * fixed formatting on base_init templating * suppressed some logging, removed some unused deps * updated build * made function for generating tarball headers * printing path for make_header, providing port in dockerfile * updated to latest spec * improved make_header trait api --------- Co-authored-by: Funny <[email protected]>
1 parent c0a99ad commit f68df95

File tree

9 files changed

+2088
-175
lines changed

9 files changed

+2088
-175
lines changed

Cargo.lock

Lines changed: 1826 additions & 164 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,12 @@ edition = "2021"
66
[dependencies]
77
anyhow = "1.0.95"
88
clap = { version = "4.5.27", features = ["derive"] }
9-
bedrock = { git = "http://github.com/basalt-rs/bedrock.git", features = ["tokio"] }
9+
bedrock = { git = "http://github.com/basalt-rs/bedrock.git", features = [
10+
"tokio",
11+
], rev = "26e27f0" }
1012
tokio = { version = "1.43.0", features = ["full"] }
13+
lazy_static = "1.5.0"
14+
tera = "1.20.0"
15+
bollard = "0.18.1"
16+
tokio-tar = "0.3.1"
17+
futures = "0.3.31"

data/basalt.Dockerfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
FROM rust:1.84 as basalt-compilation
2+
3+
RUN git clone https://github.com/basalt-rs/basalt-server
4+
5+
WORKDIR /basalt-server
6+
7+
RUN cargo build --release
8+
9+
# DO NOT EDIT UNLESS YOU KNOW WHAT YOU'RE DOING
10+
FROM fedora:rawhide as setup
11+
12+
WORKDIR /setup
13+
14+
COPY install.sh .
15+
RUN chmod +x install.sh
16+
RUN ./install.sh
17+
18+
FROM setup as execution
19+
20+
WORKDIR /execution
21+
22+
COPY --from=basalt-compilation /basalt-server/target/release/basalt-server .
23+
24+
COPY config.toml .
25+
COPY entrypoint.sh .
26+
RUN chmod +x ./entrypoint.sh
27+
28+
EXPOSE 9090
29+
ENTRYPOINT [ "./entrypoint.sh" ]
30+
CMD [ "./basalt-server", "run", "--port", "9090", "./config.toml" ]

data/entrypoint.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/sh
2+
echo "ENTRYPOINT: Initializing"
3+
echo "INIT: Running base init"
4+
{{ base_init }}
5+
6+
{% if custom_init %}
7+
echo "INIT: Running custom init"
8+
{{ custom_init }}
9+
{% endif %}
10+
11+
echo "ENTRYPOINT: Executing"
12+
exec "$@"

data/install.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
echo "INSTALL: running base install"
3+
{{ base_install }}
4+
5+
{% if custom_install %}
6+
echo "INSTALL: running custom install"
7+
{{ custom_install }}
8+
{% endif %}

src/build.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use std::path::{Path, PathBuf};
2+
3+
use anyhow::Context;
4+
use bedrock::Config;
5+
use futures::StreamExt;
6+
use lazy_static::lazy_static;
7+
use tokio::io::AsyncReadExt;
8+
use tokio_tar::Header;
9+
10+
const BASE_DOCKER_SRC: &str = include_str!("../data/basalt.Dockerfile");
11+
const INSTALL_SRC: &str = include_str!("../data/install.sh");
12+
const ENTRY_SRC: &str = include_str!("../data/entrypoint.sh");
13+
14+
lazy_static! {
15+
static ref tmpl: tera::Tera = {
16+
let mut t = tera::Tera::default();
17+
t.add_raw_template("dockerfile", BASE_DOCKER_SRC)
18+
.expect("Failed to register docker source template");
19+
t.add_raw_template("install.sh", INSTALL_SRC)
20+
.expect("Failed to register install source template");
21+
t.add_raw_template("entrypoint.sh", ENTRY_SRC)
22+
.expect("Failed to register init source template");
23+
t
24+
};
25+
}
26+
27+
pub async fn build_with_output(
28+
output: &Option<PathBuf>,
29+
config_file: &Path,
30+
tag: Option<String>,
31+
) -> anyhow::Result<()> {
32+
let mut file = tokio::fs::File::open(config_file)
33+
.await
34+
.context("Failed to open config file")?;
35+
let mut config_content = String::new();
36+
file.read_to_string(&mut config_content)
37+
.await
38+
.context("Failed to read config file to string")?;
39+
let cfg = bedrock::Config::from_str(
40+
&config_content,
41+
Some(config_file.to_str().context("Failed to get file path")?),
42+
)
43+
.context("Failed to read configuration file")?;
44+
45+
let mut tarball = tokio_tar::Builder::new(Vec::new());
46+
47+
let mut ctx = tera::Context::new();
48+
ctx.insert("base_install", &make_base_install(&cfg));
49+
ctx.insert("base_init", &make_base_init(&cfg));
50+
if let Some(setup) = &cfg.setup {
51+
if let Some(install) = &setup.install {
52+
dbg!(install.to_string());
53+
ctx.insert("custom_install", &install.trim());
54+
}
55+
if let Some(init) = &setup.init {
56+
dbg!(init.to_string());
57+
ctx.insert("custom_init", init.trim());
58+
}
59+
}
60+
let install_content = tmpl
61+
.render("install.sh", &ctx)
62+
.context("Failed to render installation script")?;
63+
let entrypoint_content = tmpl
64+
.render("entrypoint.sh", &ctx)
65+
.context("Failed to render entrypoint script")?;
66+
dbg!(&install_content);
67+
let content = tmpl
68+
.render("dockerfile", &ctx)
69+
.context("Failed to render dockerfile")?;
70+
let config_header = make_header("config.toml", config_content.len() as u64, 0o644)
71+
.context("Failed to create config header")?;
72+
tarball
73+
.append(&config_header, config_content.as_bytes())
74+
.await
75+
.context("Failed to archive config.toml")?;
76+
let dockerfile_header = make_header("Dockerfile", content.len() as u64, 0o644)
77+
.context("Failed to create dockerfile header")?;
78+
tarball
79+
.append(&dockerfile_header, content.as_bytes())
80+
.await
81+
.context("Failed to append dockerfile to tarball")?;
82+
let install_header = make_header("install.sh", install_content.len() as u64, 0o644)
83+
.context("Failed to create install header")?;
84+
tarball
85+
.append(&install_header, install_content.as_bytes())
86+
.await
87+
.context("Failed to append install.sh to tarball")?;
88+
let entrypoint_header = make_header("entrypoint.sh", entrypoint_content.len() as u64, 0o644)
89+
.context("Failed to create entrypoint header")?;
90+
tarball
91+
.append(&entrypoint_header, entrypoint_content.as_bytes())
92+
.await
93+
.context("Failed to append entrypoint.sh to tar")?;
94+
let out_data = tarball
95+
.into_inner()
96+
.await
97+
.context("Failed to finish tarball")?;
98+
match output {
99+
Some(out_path) => {
100+
tokio::fs::write(out_path, out_data)
101+
.await
102+
.context("Failed to write data")?;
103+
}
104+
None => {
105+
let docker = bollard::Docker::connect_with_local_defaults()
106+
.context("Failed to connect to docker")?;
107+
let tag = tag.unwrap_or(format!("bslt-{}", cfg.hash()));
108+
let stream = docker.build_image(
109+
bollard::image::BuildImageOptions {
110+
dockerfile: "Dockerfile",
111+
t: &tag,
112+
rm: true,
113+
..Default::default()
114+
},
115+
None,
116+
Some(out_data.into()),
117+
);
118+
119+
// Process the stream
120+
tokio::pin!(stream);
121+
while let Some(item) = stream.next().await {
122+
let msg = item.context("Failed to perform docker build")?;
123+
if let Some(stream) = msg.stream {
124+
println!(
125+
"[BUILD] {}",
126+
stream.trim().replace("\n", " ").replace("\t", " ")
127+
);
128+
}
129+
}
130+
}
131+
};
132+
Ok(())
133+
}
134+
135+
fn make_base_install(cfg: &Config) -> String {
136+
cfg.languages
137+
.iter()
138+
.map(|e| match e {
139+
bedrock::language::Language::BuiltIn { language, version } => {
140+
language.install_command(version).unwrap_or("").to_owned()
141+
}
142+
_ => "".into(),
143+
})
144+
.filter(|e| !e.is_empty())
145+
.collect::<Vec<String>>()
146+
.join("\n")
147+
.trim()
148+
.to_owned()
149+
}
150+
151+
fn make_base_init(cfg: &Config) -> String {
152+
cfg.languages
153+
.iter()
154+
.map(|e| match e {
155+
bedrock::language::Language::BuiltIn { language, version } => {
156+
language.init_command(version).unwrap_or("").to_owned()
157+
}
158+
_ => "".into(),
159+
})
160+
.filter(|e| !e.is_empty())
161+
.collect::<Vec<String>>()
162+
.join("\n")
163+
.trim()
164+
.to_owned()
165+
}
166+
167+
fn make_header<P>(path: P, size: u64, mode: u32) -> anyhow::Result<Header>
168+
where
169+
P: AsRef<Path>,
170+
{
171+
let mut header = tokio_tar::Header::new_gnu();
172+
header
173+
.set_path(&path)
174+
.with_context(|| format!("Failed to set {} tar header", path.as_ref().display()))?;
175+
header.set_size(size);
176+
header.set_mode(mode);
177+
header.set_cksum();
178+
Ok(header)
179+
}

src/cli.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ pub enum SubCmd {
1616
},
1717
/// Build the docker file based on a given configuration file
1818
Build {
19-
/// File to which the Dockerfile should be written
20-
#[arg(short, long, default_value = "./Dockerfile")]
21-
output: PathBuf,
19+
/// Specifies tag for docker image. Not recommended unless you're familiar with Docker.
20+
#[arg(short, long)]
21+
tag: Option<String>,
22+
/// Path to output tarball
23+
#[arg(short, long)]
24+
output: Option<PathBuf>,
2225
/// The configuration file to build
2326
config_file: PathBuf,
2427
},

src/main.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
mod build;
12
mod cli;
23
use std::{path::Path, process};
34

45
use bedrock::ConfigReadError;
6+
use build::build_with_output;
57
use clap::Parser;
68
use cli::Cli;
79
use tokio::fs::File;
@@ -33,9 +35,11 @@ async fn main() -> anyhow::Result<()> {
3335

3436
match cli.subcommand {
3537
cli::SubCmd::Verify { config_file } => verify(&config_file).await?,
36-
cli::SubCmd::Build { .. } => {
37-
todo!();
38-
}
38+
cli::SubCmd::Build {
39+
tag,
40+
output,
41+
config_file,
42+
} => build_with_output(&output, &config_file, tag).await?,
3943
cli::SubCmd::Run { .. } => {
4044
todo!();
4145
}

tests/one.toml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ port = 80
55

66
# install = { import = "./install.sh" }
77
install = '''
8-
apt-get install opam
8+
dnf install opam
99
'''
1010

1111
# init = { import = "./init.sh" }
@@ -14,10 +14,18 @@ opam init -y
1414
eval $(opam env)
1515
'''
1616

17+
[test_runner]
18+
# import = "./test-runner.toml"
19+
20+
timeout_ms = 60_000
21+
trim_output = true
22+
max_memory = { compile = 128, run = 64 }
23+
max_file_size = 8192
24+
1725
[languages]
18-
python3 = "enabled"
26+
python3 = "latest"
1927
java = "21"
20-
ocaml = { build = "ocamlc -o out {{SOURCEFLIE}}", run = "./out" }
28+
ocaml = { build = "ocamlc -o out solution.ml", run = "./out", source_file = "solution.ml" }
2129

2230
[[accounts.admins]]
2331
name = "Teacher"
@@ -55,7 +63,7 @@ Reversing a string is one of the most *basic* algorithmic
5563
problems for a beginner computer science student to solve.
5664
5765
Solve it.
58-
''''
66+
'''
5967

6068
[[packet.problems.tests]]
6169
input = "hello"

0 commit comments

Comments
 (0)