Skip to content

Commit d2c0359

Browse files
authored
Merge pull request #47 from kpcyrd/outdir
Use rebuilderd managed output directory
2 parents 963ac85 + abe3250 commit d2c0359

File tree

9 files changed

+349
-172
lines changed

9 files changed

+349
-172
lines changed

.github/assets/mOWZt75.png

283 KB
Loading

Cargo.lock

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

README.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,70 @@ afford to.
2626

2727
[2]: https://diffoscope.org/
2828

29-
# Setup
29+
# Accessing a rebuilderd instance in your browser
30+
31+
Many instance run a web frontend to display their results. [rebuilderd-website]
32+
is a very good choice and the software powering the Arch Linux rebuilderd
33+
instance:
34+
35+
[rebuilderd-website]: https://gitlab.archlinux.org/archlinux/rebuilderd-website
36+
37+
https://reproducible.archlinux.org/
38+
39+
Loading the index of all packages may take a short time.
40+
41+
# Scripting access to a rebuilderd instance
42+
43+
It's also possible to query and manage a rebuilderd instance in a scriptable
44+
way. It's recommended to install the `rebuildctl` commandline util to do this:
45+
46+
pacman -S rebuilderd
47+
48+
You can then query a rebuilderd instance for the status of a specific package:
49+
50+
rebuildctl -H https://reproducible.archlinux.org pkgs ls --name rebuilderd
51+
52+
You have to specify which instance you want to query because there's no
53+
definite truth™. You could ask multiple instances though, including one you
54+
operate yourself.
55+
56+
If the rebuilder seems to have outdated data or lists a package as unknown the
57+
update may still be in the build queue. You can query the build queue of an
58+
instance like this:
59+
60+
rebuildctl -H https://reproducible.archlinux.org queue ls --head
61+
62+
If there's no output that means the build queue is empty.
63+
64+
If you're the administrator of this instance you can also run commands like:
65+
66+
rebuildctl status
67+
68+
Or immediately retry all failed rebuild attempts (there's an automatic retry on
69+
by default):
70+
71+
rebuildctl pkgs requeue --status BAD --reset
72+
73+
# Running a rebuilderd instance yourself
74+
75+
![journalctl output of a rebuilderd-worker](.github/assets/mOWZt75.png)
76+
77+
"I compile everything from source" - a significant amount of real world binary
78+
packages can already be reproduced today. The more people run rebuilders, the
79+
harder it is to compromise all of them.
80+
81+
At the current stage of the project we're interested in every rebuilder there
82+
is! Most rebuilderd discussion currently happens in #archlinux-reprodubile on
83+
freenode, feel free to drop by if you're running a instance or considering
84+
setting one up. Having a few unreproducible packages is normal (even if it's
85+
slightly more than the official rebuilder), but having additional people
86+
confirm successful rebuilds is very helpful.
3087

3188
## Arch Linux
3289

3390
Please see the setup instructions in the [Arch Linux Wiki](https://wiki.archlinux.org/index.php/Rebuilderd).
3491

35-
## Development
92+
# Development
3693

3794
A rebuilder consists of the `rebuilderd` daemon and >= 1 workers:
3895

worker/rebuilder-archlinux.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/bin/sh
22
set -xe
3-
repro -- "${1}"
3+
OUTDIR="${REBUILDERD_OUTDIR}" repro -- "${1}"

worker/src/diffoscope.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ use crate::config;
22
use crate::proc;
33
use rebuilderd_common::errors::*;
44
use std::collections::HashMap;
5+
use std::ffi::OsString;
56
use std::path::Path;
67
use std::time::Duration;
78

8-
pub async fn diffoscope(a: &str, b: &str, settings: &config::Diffoscope) -> Result<String> {
9-
let mut args: Vec<&str> = settings.args.iter().map(AsRef::as_ref).collect();
10-
args.push("--");
11-
args.push(a);
12-
args.push(b);
9+
pub async fn diffoscope(a: &Path, b: &Path, settings: &config::Diffoscope) -> Result<String> {
10+
let mut args = settings.args.iter().map(OsString::from).collect::<Vec<_>>();
11+
args.push("--".into());
12+
args.push(a.into());
13+
args.push(b.into());
1314

1415
let timeout = settings.timeout.unwrap_or(3600); // 1h
1516

worker/src/download.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use futures_util::StreamExt;
22
use rebuilderd_common::errors::*;
3+
use std::path::{Path, PathBuf};
34
use tokio::fs::File;
45
use tokio::io::AsyncWriteExt;
56
use url::Url;
67

7-
pub async fn download(url: &str, tmp: &tempfile::TempDir) -> Result<(String, String)> {
8-
let url = url.parse::<Url>()
8+
pub async fn download(url_str: &str, path: &Path) -> Result<PathBuf> {
9+
let url = url_str.parse::<Url>()
910
.context("Failed to parse input as url")?;
1011

1112
let filename = url.path_segments()
@@ -16,9 +17,9 @@ pub async fn download(url: &str, tmp: &tempfile::TempDir) -> Result<(String, Str
1617
bail!("Filename is empty");
1718
}
1819

19-
let target = tmp.path().join(filename);
20+
let target = path.join(filename);
2021

21-
info!("Downloading {:?} to {:?}", url, target);
22+
info!("Downloading {:?} to {:?}", url_str, target);
2223
let client = reqwest::Client::new();
2324
let mut stream = client.get(&url.to_string())
2425
.send()
@@ -38,8 +39,5 @@ pub async fn download(url: &str, tmp: &tempfile::TempDir) -> Result<(String, Str
3839
}
3940
info!("Downloaded {} bytes", bytes);
4041

41-
let target = target.to_str()
42-
.ok_or_else(|| format_err!("Input path contains invalid characters"))?;
43-
44-
Ok((target.to_string(), filename.to_string()))
42+
Ok(PathBuf::from(filename))
4543
}

worker/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ struct Connect {
6464

6565
#[derive(Debug, StructOpt)]
6666
struct Diffoscope {
67-
pub a: String,
68-
pub b: String,
67+
pub a: PathBuf,
68+
pub b: PathBuf,
6969
}
7070

7171
async fn spawn_rebuilder_script_with_heartbeat<'a>(client: &Client, distro: &Distro, item: &QueueItem, config: &config::ConfigFile) -> Result<Rebuild> {

worker/src/proc.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use futures_util::FutureExt;
2-
use nix::unistd::Pid;
32
use nix::sys::signal::{self, Signal};
3+
use nix::unistd::Pid;
44
use rebuilderd_common::errors::*;
55
use std::collections::HashMap;
6+
use std::ffi::OsStr;
7+
use std::fmt;
68
use std::path::Path;
79
use std::process::Stdio;
810
use std::time::{Duration, Instant};
@@ -106,7 +108,10 @@ impl Capture {
106108
}
107109
}
108110

109-
pub async fn run(bin: &Path, args: &[&str], opts: Options) -> Result<(bool, String)> {
111+
pub async fn run<I, S>(bin: &Path, args: I, opts: Options) -> Result<(bool, String)>
112+
where I: IntoIterator<Item = S> + fmt::Debug,
113+
S: AsRef<OsStr>,
114+
{
110115
info!("Running {:?} {:?}", bin, args);
111116
let mut child = Command::new(bin)
112117
.args(args)

worker/src/rebuild.rs

Lines changed: 139 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use crate::config;
2-
use crate::proc;
32
use crate::diffoscope::diffoscope;
43
use crate::download::download;
4+
use crate::proc;
55
use rebuilderd_common::Distro;
66
use rebuilderd_common::api::{Rebuild, BuildStatus};
77
use rebuilderd_common::errors::*;
88
use rebuilderd_common::errors::{Context as _};
99
use std::borrow::Cow;
1010
use std::collections::HashMap;
11+
use std::fs;
12+
use std::io::ErrorKind;
1113
use std::path::{Path, PathBuf};
1214
use std::time::Duration;
15+
use tokio::fs::File;
16+
use tokio::io::AsyncReadExt;
1317

1418
pub struct Context<'a> {
1519
pub distro: &'a Distro,
@@ -40,44 +44,113 @@ fn locate_script(distro: &Distro, script_location: Option<PathBuf>) -> Result<Pa
4044
bail!("Failed to find a rebuilder backend")
4145
}
4246

47+
fn path_to_string(path: &Path) -> Result<String> {
48+
let s = path.to_str()
49+
.with_context(|| anyhow!("Path contains invalid characters: {:?}", path))?;
50+
Ok(s.to_string())
51+
}
52+
53+
pub async fn compare_files(a: &Path, b: &Path) -> Result<bool> {
54+
let mut buf1 = [0u8; 4096];
55+
let mut buf2 = [0u8; 4096];
56+
57+
info!("Comparing {:?} with {:?}", a, b);
58+
let mut f1 = File::open(a).await
59+
.with_context(|| anyhow!("Failed to open {:?}", a))?;
60+
let mut f2 = File::open(b).await
61+
.with_context(|| anyhow!("Failed to open {:?}", b))?;
62+
63+
let mut pos = 0;
64+
loop {
65+
// read up to 4k bytes from the first file
66+
let n = f1.read_buf(&mut &mut buf1[..]).await?;
67+
68+
// check if the first file is end-of-file
69+
if n == 0 {
70+
debug!("First file is at end-of-file");
71+
72+
// check if other file is eof too
73+
let n = f2.read_buf(&mut &mut buf2[..]).await?;
74+
if n > 0 {
75+
info!("Files are not identical, {:?} is longer", b);
76+
return Ok(false);
77+
} else {
78+
return Ok(true);
79+
}
80+
}
81+
82+
// check the same chunk in the other file
83+
match f2.read_exact(&mut buf2[..n]).await {
84+
Ok(n) => n,
85+
Err(err) if err.kind() == ErrorKind::UnexpectedEof => {
86+
info!("Files are not identical, {:?} is shorter", b);
87+
return Ok(false);
88+
},
89+
err => err?,
90+
};
91+
92+
if buf1[..n] != buf2[..n] {
93+
// get the exact position
94+
// this can't panic because we've already checked the slices are not equal
95+
let pos = pos + buf1[..n].iter().zip(
96+
buf2[..n].iter()
97+
).position(|(a,b)|a != b).unwrap();
98+
info!("Files {:?} and {:?} differ at position {}", a, b, pos);
99+
100+
return Ok(false);
101+
}
102+
103+
// advance the number of bytes that are equal
104+
pos += n;
105+
}
106+
}
107+
43108
pub async fn rebuild(ctx: &Context<'_>, url: &str) -> Result<Rebuild> {
109+
// setup
44110
let tmp = tempfile::Builder::new().prefix("rebuilderd").tempdir()?;
45111

46-
let (input, filename) = download(url, &tmp)
112+
let inputs_dir = tmp.path().join("inputs");
113+
fs::create_dir(&inputs_dir)
114+
.context("Failed to create inputs/ temp dir")?;
115+
116+
let out_dir = tmp.path().join("out");
117+
fs::create_dir(&out_dir)
118+
.context("Failed to create out/ temp dir")?;
119+
120+
// download
121+
let filename = download(url, &inputs_dir)
47122
.await
48123
.with_context(|| anyhow!("Failed to download original package from {:?}", url))?;
49124

50-
let (success, log) = verify(ctx, &input).await?;
51-
52-
if success {
53-
info!("Rebuilder backend indicated a success rebuild!");
125+
// rebuild
126+
let input_path = inputs_dir.join(&filename);
127+
let log = verify(ctx, &out_dir, &input_path).await?;
128+
129+
// process result
130+
let output_path = out_dir.join(&filename);
131+
if !output_path.exists() {
132+
info!("Build failed, no output artifact found at {:?}", output_path);
133+
Ok(Rebuild::new(BuildStatus::Bad, log))
134+
} else if compare_files(&input_path, &output_path).await? {
135+
info!("Files are identical, marking as GOOD");
54136
Ok(Rebuild::new(BuildStatus::Good, log))
55137
} else {
56-
info!("Rebuilder backend exited with non-zero exit code");
138+
info!("Build successful but artifacts differ");
57139
let mut res = Rebuild::new(BuildStatus::Bad, log);
58140

59141
// generate diffoscope diff if enabled
60142
if ctx.diffoscope.enabled {
61-
let output = Path::new("./build/").join(filename);
62-
if output.exists() {
63-
let output = output.to_str()
64-
.ok_or_else(|| format_err!("Output path contains invalid characters"))?;
65-
66-
let diff = diffoscope(&input, output, &ctx.diffoscope)
67-
.await
68-
.context("Failed to run diffoscope")?;
69-
res.diffoscope = Some(diff);
70-
} else {
71-
info!("Skipping diffoscope because rebuilder script did not produce output");
72-
}
143+
let diff = diffoscope(&input_path, &output_path, &ctx.diffoscope)
144+
.await
145+
.context("Failed to run diffoscope")?;
146+
res.diffoscope = Some(diff);
73147
}
74148

75149
Ok(res)
76150
}
77151
}
78152

79-
// TODO: automatically truncate logs to a max-length if configured
80-
async fn verify(ctx: &Context<'_>, path: &str) -> Result<(bool, String)> {
153+
async fn verify(ctx: &Context<'_>, out_dir: &Path, input_path: &Path) -> Result<String> {
81154
let bin = if let Some(script) = ctx.script_location {
82155
Cow::Borrowed(script)
83156
} else {
@@ -86,15 +159,10 @@ async fn verify(ctx: &Context<'_>, path: &str) -> Result<(bool, String)> {
86159
Cow::Owned(script)
87160
};
88161

89-
// TODO: establish a common interface to interface with distro rebuilders
90-
// TODO: specify the path twice because the 2nd argument used to be the path
91-
// TODO: we want to move this to the first instead. the 2nd argument can be removed in the future
92-
let args = &[path, path];
93-
94162
let timeout = ctx.build.timeout.unwrap_or(3600 * 24); // 24h
95163

96164
let mut envs = HashMap::new();
97-
envs.insert("REBUILDERD_OUTDIR".into(), "./build".into());
165+
envs.insert("REBUILDERD_OUTDIR".into(), path_to_string(out_dir)?);
98166

99167
let opts = proc::Options {
100168
timeout: Duration::from_secs(timeout),
@@ -103,5 +171,48 @@ async fn verify(ctx: &Context<'_>, path: &str) -> Result<(bool, String)> {
103171
passthrough: !ctx.build.silent,
104172
envs,
105173
};
106-
proc::run(bin.as_ref(), args, opts).await
174+
let (_success, log) = proc::run(bin.as_ref(), &[input_path], opts).await?;
175+
176+
Ok(log)
177+
}
178+
179+
#[cfg(test)]
180+
mod tests {
181+
use super::*;
182+
183+
#[tokio::test]
184+
async fn compare_files_equal() {
185+
let equal = compare_files(Path::new("src/main.rs"), Path::new("src/main.rs")).await.unwrap();
186+
assert!(equal);
187+
}
188+
189+
#[tokio::test]
190+
async fn compare_files_not_equal1() {
191+
let equal = compare_files(Path::new("src/main.rs"), Path::new("Cargo.toml")).await.unwrap();
192+
assert!(!equal);
193+
}
194+
195+
#[tokio::test]
196+
async fn compare_files_not_equal2() {
197+
let equal = compare_files(Path::new("Cargo.toml"), Path::new("src/main.rs")).await.unwrap();
198+
assert!(!equal);
199+
}
200+
201+
#[tokio::test]
202+
async fn compare_large_files_equal() {
203+
let dir = tempfile::tempdir().unwrap();
204+
fs::write(dir.path().join("a"), &[0u8; 4096 * 100]).unwrap();
205+
fs::write(dir.path().join("b"), &[0u8; 4096 * 100]).unwrap();
206+
let equal = compare_files(&dir.path().join("a"), &dir.path().join("b")).await.unwrap();
207+
assert!(equal);
208+
}
209+
210+
#[tokio::test]
211+
async fn compare_large_files_not_equal() {
212+
let dir = tempfile::tempdir().unwrap();
213+
fs::write(dir.path().join("a"), &[0u8; 4096 * 100]).unwrap();
214+
fs::write(dir.path().join("b"), &[1u8; 4096 * 100]).unwrap();
215+
let equal = compare_files(&dir.path().join("a"), &dir.path().join("b")).await.unwrap();
216+
assert!(!equal);
217+
}
107218
}

0 commit comments

Comments
 (0)