mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-21 22:34:05 +00:00
Add support for test suites emulated in QEMU
This commit adds support to the build system to execute test suites that cannot run natively but can instead run inside of a QEMU emulator. A proof-of-concept builder was added for the `arm-unknown-linux-gnueabihf` target to show off how this might work. In general the architecture is to have a server running inside of the emulator which a local client connects to. The protocol between the server/client supports compiling tests on the host and running them on the target inside the emulator. Closes #33114
This commit is contained in:
parent
4be49e1937
commit
1747ce25ad
@ -13,6 +13,7 @@ matrix:
|
||||
include:
|
||||
# Linux builders, all docker images
|
||||
- env: IMAGE=android DEPLOY=1
|
||||
- env: IMAGE=armhf-gnu
|
||||
- env: IMAGE=cross DEPLOY=1
|
||||
- env: IMAGE=linux-tested-targets DEPLOY=1
|
||||
- env: IMAGE=dist-arm-linux DEPLOY=1
|
||||
|
1
configure
vendored
1
configure
vendored
@ -684,6 +684,7 @@ valopt musl-root-arm "" "arm-unknown-linux-musleabi install directory"
|
||||
valopt musl-root-armhf "" "arm-unknown-linux-musleabihf install directory"
|
||||
valopt musl-root-armv7 "" "armv7-unknown-linux-musleabihf install directory"
|
||||
valopt extra-filename "" "Additional data that is hashed and passed to the -C extra-filename flag"
|
||||
valopt qemu-armhf-rootfs "" "rootfs in qemu testing, you probably don't want to use this"
|
||||
|
||||
if [ -e ${CFG_SRC_DIR}.git ]
|
||||
then
|
||||
|
8
src/Cargo.lock
generated
8
src/Cargo.lock
generated
@ -221,6 +221,14 @@ dependencies = [
|
||||
"syntax_pos 0.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qemu-test-client"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "qemu-test-server"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.0.0"
|
||||
|
@ -11,6 +11,8 @@ members = [
|
||||
"tools/rustbook",
|
||||
"tools/tidy",
|
||||
"tools/build-manifest",
|
||||
"tools/qemu-test-client",
|
||||
"tools/qemu-test-server",
|
||||
]
|
||||
|
||||
# Curiously, compiletest will segfault if compiled with opt-level=3 on 64-bit
|
||||
|
@ -26,7 +26,7 @@ use build_helper::output;
|
||||
|
||||
use {Build, Compiler, Mode};
|
||||
use dist;
|
||||
use util::{self, dylib_path, dylib_path_var};
|
||||
use util::{self, dylib_path, dylib_path_var, exe};
|
||||
|
||||
const ADB_TEST_DIR: &'static str = "/data/tmp";
|
||||
|
||||
@ -221,6 +221,12 @@ pub fn compiletest(build: &Build,
|
||||
.arg("--llvm-cxxflags").arg("");
|
||||
}
|
||||
|
||||
if build.qemu_rootfs(target).is_some() {
|
||||
cmd.arg("--qemu-test-client")
|
||||
.arg(build.tool(&Compiler::new(0, &build.config.build),
|
||||
"qemu-test-client"));
|
||||
}
|
||||
|
||||
// Running a C compiler on MSVC requires a few env vars to be set, to be
|
||||
// sure to set them here.
|
||||
//
|
||||
@ -403,9 +409,9 @@ pub fn krate(build: &Build,
|
||||
dylib_path.insert(0, build.sysroot_libdir(&compiler, target));
|
||||
cargo.env(dylib_path_var(), env::join_paths(&dylib_path).unwrap());
|
||||
|
||||
if target.contains("android") {
|
||||
cargo.arg("--no-run");
|
||||
} else if target.contains("emscripten") {
|
||||
if target.contains("android") ||
|
||||
target.contains("emscripten") ||
|
||||
build.qemu_rootfs(target).is_some() {
|
||||
cargo.arg("--no-run");
|
||||
}
|
||||
|
||||
@ -423,6 +429,9 @@ pub fn krate(build: &Build,
|
||||
} else if target.contains("emscripten") {
|
||||
build.run(&mut cargo);
|
||||
krate_emscripten(build, &compiler, target, mode);
|
||||
} else if build.qemu_rootfs(target).is_some() {
|
||||
build.run(&mut cargo);
|
||||
krate_qemu(build, &compiler, target, mode);
|
||||
} else {
|
||||
cargo.args(&build.flags.cmd.test_args());
|
||||
build.run(&mut cargo);
|
||||
@ -480,23 +489,46 @@ fn krate_emscripten(build: &Build,
|
||||
compiler: &Compiler,
|
||||
target: &str,
|
||||
mode: Mode) {
|
||||
let mut tests = Vec::new();
|
||||
let out_dir = build.cargo_out(compiler, mode, target);
|
||||
find_tests(&out_dir, target, &mut tests);
|
||||
find_tests(&out_dir.join("deps"), target, &mut tests);
|
||||
let mut tests = Vec::new();
|
||||
let out_dir = build.cargo_out(compiler, mode, target);
|
||||
find_tests(&out_dir, target, &mut tests);
|
||||
find_tests(&out_dir.join("deps"), target, &mut tests);
|
||||
|
||||
for test in tests {
|
||||
let test_file_name = test.to_string_lossy().into_owned();
|
||||
println!("running {}", test_file_name);
|
||||
let nodejs = build.config.nodejs.as_ref().expect("nodejs not configured");
|
||||
let mut cmd = Command::new(nodejs);
|
||||
cmd.arg(&test_file_name);
|
||||
if build.config.quiet_tests {
|
||||
cmd.arg("--quiet");
|
||||
}
|
||||
build.run(&mut cmd);
|
||||
}
|
||||
}
|
||||
for test in tests {
|
||||
let test_file_name = test.to_string_lossy().into_owned();
|
||||
println!("running {}", test_file_name);
|
||||
let nodejs = build.config.nodejs.as_ref().expect("nodejs not configured");
|
||||
let mut cmd = Command::new(nodejs);
|
||||
cmd.arg(&test_file_name);
|
||||
if build.config.quiet_tests {
|
||||
cmd.arg("--quiet");
|
||||
}
|
||||
build.run(&mut cmd);
|
||||
}
|
||||
}
|
||||
|
||||
fn krate_qemu(build: &Build,
|
||||
compiler: &Compiler,
|
||||
target: &str,
|
||||
mode: Mode) {
|
||||
let mut tests = Vec::new();
|
||||
let out_dir = build.cargo_out(compiler, mode, target);
|
||||
find_tests(&out_dir, target, &mut tests);
|
||||
find_tests(&out_dir.join("deps"), target, &mut tests);
|
||||
|
||||
let tool = build.tool(&Compiler::new(0, &build.config.build),
|
||||
"qemu-test-client");
|
||||
for test in tests {
|
||||
let mut cmd = Command::new(&tool);
|
||||
cmd.arg("run")
|
||||
.arg(&test);
|
||||
if build.config.quiet_tests {
|
||||
cmd.arg("--quiet");
|
||||
}
|
||||
cmd.args(&build.flags.cmd.test_args());
|
||||
build.run(&mut cmd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn find_tests(dir: &Path,
|
||||
@ -516,13 +548,15 @@ fn find_tests(dir: &Path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn android_copy_libs(build: &Build,
|
||||
compiler: &Compiler,
|
||||
target: &str) {
|
||||
if !target.contains("android") {
|
||||
return
|
||||
pub fn emulator_copy_libs(build: &Build, compiler: &Compiler, target: &str) {
|
||||
if target.contains("android") {
|
||||
android_copy_libs(build, compiler, target)
|
||||
} else if let Some(s) = build.qemu_rootfs(target) {
|
||||
qemu_copy_libs(build, compiler, target, s)
|
||||
}
|
||||
}
|
||||
|
||||
fn android_copy_libs(build: &Build, compiler: &Compiler, target: &str) {
|
||||
println!("Android copy libs to emulator ({})", target);
|
||||
build.run(Command::new("adb").arg("wait-for-device"));
|
||||
build.run(Command::new("adb").arg("remount"));
|
||||
@ -548,6 +582,39 @@ pub fn android_copy_libs(build: &Build,
|
||||
}
|
||||
}
|
||||
|
||||
fn qemu_copy_libs(build: &Build,
|
||||
compiler: &Compiler,
|
||||
target: &str,
|
||||
rootfs: &Path) {
|
||||
println!("QEMU copy libs to emulator ({})", target);
|
||||
assert!(target.starts_with("arm"), "only works with arm for now");
|
||||
t!(fs::create_dir_all(build.out.join("tmp")));
|
||||
|
||||
// Copy our freshly compiled test server over to the rootfs
|
||||
let server = build.cargo_out(compiler, Mode::Tool, target)
|
||||
.join(exe("qemu-test-server", target));
|
||||
t!(fs::copy(&server, rootfs.join("testd")));
|
||||
|
||||
// Spawn the emulator and wait for it to come online
|
||||
let tool = build.tool(&Compiler::new(0, &build.config.build),
|
||||
"qemu-test-client");
|
||||
build.run(Command::new(&tool)
|
||||
.arg("spawn-emulator")
|
||||
.arg(rootfs)
|
||||
.arg(build.out.join("tmp")));
|
||||
|
||||
// Push all our dylibs to the emulator
|
||||
for f in t!(build.sysroot_libdir(compiler, target).read_dir()) {
|
||||
let f = t!(f);
|
||||
let name = f.file_name().into_string().unwrap();
|
||||
if util::is_dylib(&name) {
|
||||
build.run(Command::new(&tool)
|
||||
.arg("push")
|
||||
.arg(f.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run "distcheck", a 'make check' from a tarball
|
||||
pub fn distcheck(build: &Build) {
|
||||
if build.config.build != "x86_64-unknown-linux-gnu" {
|
||||
|
@ -382,10 +382,10 @@ fn add_to_sysroot(out_dir: &Path, sysroot_dst: &Path) {
|
||||
///
|
||||
/// This will build the specified tool with the specified `host` compiler in
|
||||
/// `stage` into the normal cargo output directory.
|
||||
pub fn tool(build: &Build, stage: u32, host: &str, tool: &str) {
|
||||
println!("Building stage{} tool {} ({})", stage, tool, host);
|
||||
pub fn tool(build: &Build, stage: u32, target: &str, tool: &str) {
|
||||
println!("Building stage{} tool {} ({})", stage, tool, target);
|
||||
|
||||
let compiler = Compiler::new(stage, host);
|
||||
let compiler = Compiler::new(stage, &build.config.build);
|
||||
|
||||
// FIXME: need to clear out previous tool and ideally deps, may require
|
||||
// isolating output directories or require a pseudo shim step to
|
||||
@ -396,7 +396,7 @@ pub fn tool(build: &Build, stage: u32, host: &str, tool: &str) {
|
||||
// let out_dir = build.cargo_out(stage, &host, Mode::Librustc, target);
|
||||
// build.clear_if_dirty(&out_dir, &libstd_stamp(build, stage, &host, target));
|
||||
|
||||
let mut cargo = build.cargo(&compiler, Mode::Tool, host, "build");
|
||||
let mut cargo = build.cargo(&compiler, Mode::Tool, target, "build");
|
||||
cargo.arg("--manifest-path")
|
||||
.arg(build.src.join(format!("src/tools/{}/Cargo.toml", tool)));
|
||||
|
||||
|
@ -114,6 +114,7 @@ pub struct Target {
|
||||
pub cxx: Option<PathBuf>,
|
||||
pub ndk: Option<PathBuf>,
|
||||
pub musl_root: Option<PathBuf>,
|
||||
pub qemu_rootfs: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Structure of the `config.toml` file that configuration is read from.
|
||||
@ -222,6 +223,7 @@ struct TomlTarget {
|
||||
cxx: Option<String>,
|
||||
android_ndk: Option<String>,
|
||||
musl_root: Option<String>,
|
||||
qemu_rootfs: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -360,6 +362,7 @@ impl Config {
|
||||
target.cxx = cfg.cxx.clone().map(PathBuf::from);
|
||||
target.cc = cfg.cc.clone().map(PathBuf::from);
|
||||
target.musl_root = cfg.musl_root.clone().map(PathBuf::from);
|
||||
target.qemu_rootfs = cfg.qemu_rootfs.clone().map(PathBuf::from);
|
||||
|
||||
config.target_config.insert(triple.clone(), target);
|
||||
}
|
||||
@ -562,6 +565,12 @@ impl Config {
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
}
|
||||
"CFG_QEMU_ARMHF_ROOTFS" if value.len() > 0 => {
|
||||
let target = "arm-unknown-linux-gnueabihf".to_string();
|
||||
let target = self.target_config.entry(target)
|
||||
.or_insert(Target::default());
|
||||
target.qemu_rootfs = Some(parse_configure_path(value));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -886,6 +886,17 @@ impl Build {
|
||||
.map(|p| &**p)
|
||||
}
|
||||
|
||||
/// Returns the root of the "rootfs" image that this target will be using,
|
||||
/// if one was configured.
|
||||
///
|
||||
/// If `Some` is returned then that means that tests for this target are
|
||||
/// emulated with QEMU and binaries will need to be shipped to the emulator.
|
||||
fn qemu_rootfs(&self, target: &str) -> Option<&Path> {
|
||||
self.config.target_config.get(target)
|
||||
.and_then(|t| t.qemu_rootfs.as_ref())
|
||||
.map(|p| &**p)
|
||||
}
|
||||
|
||||
/// Path to the python interpreter to use
|
||||
fn python(&self) -> &Path {
|
||||
self.config.python.as_ref().unwrap()
|
||||
|
@ -295,7 +295,7 @@ pub fn build_rules<'a>(build: &'a Build) -> Rules {
|
||||
.dep(|s| s.name("libtest"))
|
||||
.dep(|s| s.name("tool-compiletest").target(s.host).stage(0))
|
||||
.dep(|s| s.name("test-helpers"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.default(mode != "pretty") // pretty tests don't run everywhere
|
||||
.run(move |s| {
|
||||
check::compiletest(build, &s.compiler(), s.target, mode, dir)
|
||||
@ -333,7 +333,7 @@ pub fn build_rules<'a>(build: &'a Build) -> Rules {
|
||||
.dep(|s| s.name("tool-compiletest").target(s.host).stage(0))
|
||||
.dep(|s| s.name("test-helpers"))
|
||||
.dep(|s| s.name("debugger-scripts"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.run(move |s| check::compiletest(build, &s.compiler(), s.target,
|
||||
"debuginfo-gdb", "debuginfo"));
|
||||
let mut rule = rules.test("check-debuginfo", "src/test/debuginfo");
|
||||
@ -387,14 +387,14 @@ pub fn build_rules<'a>(build: &'a Build) -> Rules {
|
||||
for (krate, path, _default) in krates("std_shim") {
|
||||
rules.test(&krate.test_step, path)
|
||||
.dep(|s| s.name("libtest"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.run(move |s| check::krate(build, &s.compiler(), s.target,
|
||||
Mode::Libstd, TestKind::Test,
|
||||
Some(&krate.name)));
|
||||
}
|
||||
rules.test("check-std-all", "path/to/nowhere")
|
||||
.dep(|s| s.name("libtest"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.default(true)
|
||||
.run(move |s| check::krate(build, &s.compiler(), s.target,
|
||||
Mode::Libstd, TestKind::Test, None));
|
||||
@ -403,14 +403,14 @@ pub fn build_rules<'a>(build: &'a Build) -> Rules {
|
||||
for (krate, path, _default) in krates("std_shim") {
|
||||
rules.bench(&krate.bench_step, path)
|
||||
.dep(|s| s.name("libtest"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.run(move |s| check::krate(build, &s.compiler(), s.target,
|
||||
Mode::Libstd, TestKind::Bench,
|
||||
Some(&krate.name)));
|
||||
}
|
||||
rules.bench("bench-std-all", "path/to/nowhere")
|
||||
.dep(|s| s.name("libtest"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.default(true)
|
||||
.run(move |s| check::krate(build, &s.compiler(), s.target,
|
||||
Mode::Libstd, TestKind::Bench, None));
|
||||
@ -418,21 +418,21 @@ pub fn build_rules<'a>(build: &'a Build) -> Rules {
|
||||
for (krate, path, _default) in krates("test_shim") {
|
||||
rules.test(&krate.test_step, path)
|
||||
.dep(|s| s.name("libtest"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.run(move |s| check::krate(build, &s.compiler(), s.target,
|
||||
Mode::Libtest, TestKind::Test,
|
||||
Some(&krate.name)));
|
||||
}
|
||||
rules.test("check-test-all", "path/to/nowhere")
|
||||
.dep(|s| s.name("libtest"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.default(true)
|
||||
.run(move |s| check::krate(build, &s.compiler(), s.target,
|
||||
Mode::Libtest, TestKind::Test, None));
|
||||
for (krate, path, _default) in krates("rustc-main") {
|
||||
rules.test(&krate.test_step, path)
|
||||
.dep(|s| s.name("librustc"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.host(true)
|
||||
.run(move |s| check::krate(build, &s.compiler(), s.target,
|
||||
Mode::Librustc, TestKind::Test,
|
||||
@ -440,7 +440,7 @@ pub fn build_rules<'a>(build: &'a Build) -> Rules {
|
||||
}
|
||||
rules.test("check-rustc-all", "path/to/nowhere")
|
||||
.dep(|s| s.name("librustc"))
|
||||
.dep(|s| s.name("android-copy-libs"))
|
||||
.dep(|s| s.name("emulator-copy-libs"))
|
||||
.default(true)
|
||||
.host(true)
|
||||
.run(move |s| check::krate(build, &s.compiler(), s.target,
|
||||
@ -481,9 +481,34 @@ pub fn build_rules<'a>(build: &'a Build) -> Rules {
|
||||
|
||||
rules.build("test-helpers", "src/rt/rust_test_helpers.c")
|
||||
.run(move |s| native::test_helpers(build, s.target));
|
||||
rules.test("android-copy-libs", "path/to/nowhere")
|
||||
|
||||
// Some test suites are run inside emulators, and most of our test binaries
|
||||
// are linked dynamically which means we need to ship the standard library
|
||||
// and such to the emulator ahead of time. This step represents this and is
|
||||
// a dependency of all test suites.
|
||||
//
|
||||
// Most of the time this step is a noop (the `check::emulator_copy_libs`
|
||||
// only does work if necessary). For some steps such as shipping data to
|
||||
// QEMU we have to build our own tools so we've got conditional dependencies
|
||||
// on those programs as well. Note that the QEMU client is built for the
|
||||
// build target (us) and the server is built for the target.
|
||||
rules.test("emulator-copy-libs", "path/to/nowhere")
|
||||
.dep(|s| s.name("libtest"))
|
||||
.run(move |s| check::android_copy_libs(build, &s.compiler(), s.target));
|
||||
.dep(move |s| {
|
||||
if build.qemu_rootfs(s.target).is_some() {
|
||||
s.name("tool-qemu-test-client").target(s.host).stage(0)
|
||||
} else {
|
||||
Step::noop()
|
||||
}
|
||||
})
|
||||
.dep(move |s| {
|
||||
if build.qemu_rootfs(s.target).is_some() {
|
||||
s.name("tool-qemu-test-server")
|
||||
} else {
|
||||
Step::noop()
|
||||
}
|
||||
})
|
||||
.run(move |s| check::emulator_copy_libs(build, &s.compiler(), s.target));
|
||||
|
||||
rules.test("check-bootstrap", "src/bootstrap")
|
||||
.default(true)
|
||||
@ -516,6 +541,12 @@ pub fn build_rules<'a>(build: &'a Build) -> Rules {
|
||||
rules.build("tool-build-manifest", "src/tools/build-manifest")
|
||||
.dep(|s| s.name("libstd"))
|
||||
.run(move |s| compile::tool(build, s.stage, s.target, "build-manifest"));
|
||||
rules.build("tool-qemu-test-server", "src/tools/qemu-test-server")
|
||||
.dep(|s| s.name("libstd"))
|
||||
.run(move |s| compile::tool(build, s.stage, s.target, "qemu-test-server"));
|
||||
rules.build("tool-qemu-test-client", "src/tools/qemu-test-client")
|
||||
.dep(|s| s.name("libstd"))
|
||||
.run(move |s| compile::tool(build, s.stage, s.target, "qemu-test-client"));
|
||||
|
||||
// ========================================================================
|
||||
// Documentation targets
|
||||
|
90
src/ci/docker/armhf-gnu/Dockerfile
Normal file
90
src/ci/docker/armhf-gnu/Dockerfile
Normal file
@ -0,0 +1,90 @@
|
||||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update -y && apt-get install -y --no-install-recommends \
|
||||
bc \
|
||||
bzip2 \
|
||||
ca-certificates \
|
||||
cmake \
|
||||
cpio \
|
||||
curl \
|
||||
file \
|
||||
g++ \
|
||||
gcc-arm-linux-gnueabihf \
|
||||
git \
|
||||
libc6-dev \
|
||||
libc6-dev-armhf-cross \
|
||||
make \
|
||||
python2.7 \
|
||||
qemu-system-arm \
|
||||
xz-utils
|
||||
|
||||
ENV ARCH=arm \
|
||||
CROSS_COMPILE=arm-linux-gnueabihf-
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Compile the kernel that we're going to run and be emulating with. This is
|
||||
# basically just done to be compatible with the QEMU target that we're going
|
||||
# to be using when running tests. If any other kernel works or if any
|
||||
# other QEMU target works with some other stock kernel, we can use that too!
|
||||
#
|
||||
# The `vexpress_config` config file was a previously generated config file for
|
||||
# the kernel. This file was generated by running `make vexpress_defconfig`
|
||||
# followed by `make menuconfig` and then enabling the IPv6 protocol page.
|
||||
COPY vexpress_config /build/.config
|
||||
RUN curl https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.4.42.tar.xz | \
|
||||
tar xJf - && \
|
||||
cd /build/linux-4.4.42 && \
|
||||
cp /build/.config . && \
|
||||
make -j$(nproc) all && \
|
||||
cp arch/arm/boot/zImage /tmp && \
|
||||
cd /build && \
|
||||
rm -rf linux-4.4.42
|
||||
|
||||
# Compile an instance of busybox as this provides a lightweight system and init
|
||||
# binary which we will boot into. Only trick here is configuring busybox to
|
||||
# build static binaries.
|
||||
RUN curl https://www.busybox.net/downloads/busybox-1.21.1.tar.bz2 | tar xjf - && \
|
||||
cd busybox-1.21.1 && \
|
||||
make defconfig && \
|
||||
sed -i 's/.*CONFIG_STATIC.*/CONFIG_STATIC=y/' .config && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
mv _install /tmp/rootfs && \
|
||||
cd /build && \
|
||||
rm -rf busybox-1.12.1
|
||||
|
||||
# Download the ubuntu rootfs, which we'll use as a chroot for all our tests.
|
||||
WORKDIR /tmp
|
||||
RUN mkdir rootfs/ubuntu
|
||||
RUN curl http://cdimage.ubuntu.com/ubuntu-base/releases/16.04/release/ubuntu-base-16.04-core-armhf.tar.gz | \
|
||||
tar xzf - -C rootfs/ubuntu && \
|
||||
cd rootfs && mkdir proc sys dev etc etc/init.d
|
||||
|
||||
# Copy over our init script, which starts up our test server and also a few
|
||||
# other misc tasks.
|
||||
COPY rcS rootfs/etc/init.d/rcS
|
||||
RUN chmod +x rootfs/etc/init.d/rcS
|
||||
|
||||
# Helper to quickly fill the entropy pool in the kernel.
|
||||
COPY addentropy.c /tmp/
|
||||
RUN arm-linux-gnueabihf-gcc addentropy.c -o rootfs/addentropy -static
|
||||
|
||||
# TODO: What is this?!
|
||||
RUN curl -O http://ftp.nl.debian.org/debian/dists/jessie/main/installer-armhf/current/images/device-tree/vexpress-v2p-ca15-tc1.dtb
|
||||
|
||||
ENV SCCACHE_DIGEST=7237e38e029342fa27b7ac25412cb9d52554008b12389727320bd533fd7f05b6a96d55485f305caf95e5c8f5f97c3313e10012ccad3e752aba2518f3522ba783
|
||||
RUN curl -L https://api.pub.build.mozilla.org/tooltool/sha512/$SCCACHE_DIGEST | \
|
||||
tar xJf - -C /usr/local/bin --strip-components=1
|
||||
|
||||
RUN curl -OL https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64.deb && \
|
||||
dpkg -i dumb-init_*.deb && \
|
||||
rm dumb-init_*.deb
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
ENV RUST_CONFIGURE_ARGS \
|
||||
--target=arm-unknown-linux-gnueabihf \
|
||||
--qemu-armhf-rootfs=/tmp/rootfs
|
||||
ENV SCRIPT python2.7 ../x.py test --target arm-unknown-linux-gnueabihf
|
||||
|
||||
ENV NO_CHANGE_USER=1
|
43
src/ci/docker/armhf-gnu/addentropy.c
Normal file
43
src/ci/docker/armhf-gnu/addentropy.c
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution and at
|
||||
// http://rust-lang.org/COPYRIGHT.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/random.h>
|
||||
|
||||
#define N 2048
|
||||
|
||||
struct entropy {
|
||||
int ent_count;
|
||||
int size;
|
||||
unsigned char data[N];
|
||||
};
|
||||
|
||||
int main() {
|
||||
struct entropy buf;
|
||||
ssize_t n;
|
||||
|
||||
int random_fd = open("/dev/random", O_RDWR);
|
||||
assert(random_fd >= 0);
|
||||
|
||||
while ((n = read(0, &buf.data, N)) > 0) {
|
||||
buf.ent_count = n * 8;
|
||||
buf.size = n;
|
||||
if (ioctl(random_fd, RNDADDENTROPY, &buf) != 0) {
|
||||
perror("failed to add entropy");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
28
src/ci/docker/armhf-gnu/rcS
Normal file
28
src/ci/docker/armhf-gnu/rcS
Normal file
@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
mount -t proc none /proc
|
||||
mount -t sysfs none /sys
|
||||
/sbin/mdev -s
|
||||
|
||||
# fill up our entropy pool, if we don't do this then anything with a hash map
|
||||
# will likely block forever as the kernel is pretty unlikely to have enough
|
||||
# entropy.
|
||||
/addentropy < /addentropy
|
||||
cat /dev/urandom | head -n 2048 | /addentropy
|
||||
|
||||
# Set up IP that qemu expects. This confgures eth0 with the public IP that QEMU
|
||||
# will communicate to as well as the loopback 127.0.0.1 address.
|
||||
ifconfig eth0 10.0.2.15
|
||||
ifconfig lo up
|
||||
|
||||
# Configure DNS resolution of 'localhost' to work
|
||||
echo 'hosts: files dns' >> /ubuntu/etc/nsswitch.conf
|
||||
echo '127.0.0.1 localhost' >> /ubuntu/etc/hosts
|
||||
|
||||
# prepare the chroot
|
||||
mount -t proc proc /ubuntu/proc/
|
||||
mount --rbind /sys /ubuntu/sys/
|
||||
mount --rbind /dev /ubuntu/dev/
|
||||
|
||||
# Execute our `testd` inside the ubuntu chroot
|
||||
cp /testd /ubuntu/testd
|
||||
chroot /ubuntu /testd &
|
2910
src/ci/docker/armhf-gnu/vexpress_config
Normal file
2910
src/ci/docker/armhf-gnu/vexpress_config
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,11 +11,13 @@
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$LOCAL_USER_ID" != "" ]; then
|
||||
useradd --shell /bin/bash -u $LOCAL_USER_ID -o -c "" -m user
|
||||
export HOME=/home/user
|
||||
unset LOCAL_USER_ID
|
||||
exec su --preserve-environment -c "env PATH=$PATH \"$0\"" user
|
||||
if [ "$NO_CHANGE_USER" = "" ]; then
|
||||
if [ "$LOCAL_USER_ID" != "" ]; then
|
||||
useradd --shell /bin/bash -u $LOCAL_USER_ID -o -c "" -m user
|
||||
export HOME=/home/user
|
||||
unset LOCAL_USER_ID
|
||||
exec su --preserve-environment -c "env PATH=$PATH \"$0\"" user
|
||||
fi
|
||||
fi
|
||||
|
||||
RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --enable-sccache"
|
||||
|
@ -439,6 +439,10 @@ mod tests {
|
||||
#[test]
|
||||
#[cfg_attr(target_os = "macos", ignore)]
|
||||
#[cfg_attr(target_os = "nacl", ignore)] // no signals on NaCl.
|
||||
// When run under our current QEMU emulation test suite this test fails,
|
||||
// although the reason isn't very clear as to why. For now this test is
|
||||
// ignored there.
|
||||
#[cfg_attr(target_arch = "arm", ignore)]
|
||||
fn test_process_mask() {
|
||||
unsafe {
|
||||
// Test to make sure that a signal mask does not get inherited.
|
||||
@ -471,7 +475,7 @@ mod tests {
|
||||
// Either EOF or failure (EPIPE) is okay.
|
||||
let mut buf = [0; 5];
|
||||
if let Ok(ret) = stdout_read.read(&mut buf) {
|
||||
assert!(ret == 0);
|
||||
assert_eq!(ret, 0);
|
||||
}
|
||||
|
||||
t!(cat.wait());
|
||||
|
@ -35,7 +35,7 @@ fn main() {
|
||||
}
|
||||
|
||||
fn parent() {
|
||||
let file = File::open(file!()).unwrap();
|
||||
let file = File::open(env::current_exe().unwrap()).unwrap();
|
||||
let tcp1 = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let tcp2 = tcp1.try_clone().unwrap();
|
||||
let addr = tcp1.local_addr().unwrap();
|
||||
|
@ -185,6 +185,9 @@ pub struct Config {
|
||||
// Print one character per test instead of one line
|
||||
pub quiet: bool,
|
||||
|
||||
// where to find the qemu test client process, if we're using it
|
||||
pub qemu_test_client: Option<PathBuf>,
|
||||
|
||||
// Configuration for various run-make tests frobbing things like C compilers
|
||||
// or querying about various LLVM component information.
|
||||
pub cc: String,
|
||||
|
@ -116,6 +116,7 @@ pub fn parse_config(args: Vec<String> ) -> Config {
|
||||
reqopt("", "llvm-components", "list of LLVM components built in", "LIST"),
|
||||
reqopt("", "llvm-cxxflags", "C++ flags for LLVM", "FLAGS"),
|
||||
optopt("", "nodejs", "the name of nodejs", "PATH"),
|
||||
optopt("", "qemu-test-client", "path to the qemu test client", "PATH"),
|
||||
optflag("h", "help", "show this message")];
|
||||
|
||||
let (argv0, args_) = args.split_first().unwrap();
|
||||
@ -196,6 +197,7 @@ pub fn parse_config(args: Vec<String> ) -> Config {
|
||||
lldb_python_dir: matches.opt_str("lldb-python-dir"),
|
||||
verbose: matches.opt_present("verbose"),
|
||||
quiet: matches.opt_present("quiet"),
|
||||
qemu_test_client: matches.opt_str("qemu-test-client").map(PathBuf::from),
|
||||
|
||||
cc: matches.opt_str("cc").unwrap(),
|
||||
cxx: matches.opt_str("cxx").unwrap(),
|
||||
@ -302,6 +304,14 @@ pub fn run_tests(config: &Config) {
|
||||
// time.
|
||||
env::set_var("RUST_TEST_THREADS", "1");
|
||||
}
|
||||
|
||||
DebugInfoGdb => {
|
||||
if config.qemu_test_client.is_some() {
|
||||
println!("WARNING: debuginfo tests are not available when \
|
||||
testing with QEMU");
|
||||
return
|
||||
}
|
||||
}
|
||||
_ => { /* proceed */ }
|
||||
}
|
||||
|
||||
|
@ -1190,7 +1190,45 @@ actual:\n\
|
||||
"arm-linux-androideabi" | "armv7-linux-androideabi" | "aarch64-linux-android" => {
|
||||
self._arm_exec_compiled_test(env)
|
||||
}
|
||||
_=> {
|
||||
|
||||
// This is pretty similar to below, we're transforming:
|
||||
//
|
||||
// program arg1 arg2
|
||||
//
|
||||
// into
|
||||
//
|
||||
// qemu-test-client run program:support-lib.so arg1 arg2
|
||||
//
|
||||
// The test-client program will upload `program` to the emulator
|
||||
// along with all other support libraries listed (in this case
|
||||
// `support-lib.so`. It will then execute the program on the
|
||||
// emulator with the arguments specified (in the environment we give
|
||||
// the process) and then report back the same result.
|
||||
_ if self.config.qemu_test_client.is_some() => {
|
||||
let aux_dir = self.aux_output_dir_name();
|
||||
let mut args = self.make_run_args();
|
||||
let mut program = args.prog.clone();
|
||||
if let Ok(entries) = aux_dir.read_dir() {
|
||||
for entry in entries {
|
||||
let entry = entry.unwrap();
|
||||
if !entry.path().is_file() {
|
||||
continue
|
||||
}
|
||||
program.push_str(":");
|
||||
program.push_str(entry.path().to_str().unwrap());
|
||||
}
|
||||
}
|
||||
args.args.insert(0, program);
|
||||
args.args.insert(0, "run".to_string());
|
||||
args.prog = self.config.qemu_test_client.clone().unwrap()
|
||||
.into_os_string().into_string().unwrap();
|
||||
self.compose_and_run(args,
|
||||
env,
|
||||
self.config.run_lib_path.to_str().unwrap(),
|
||||
Some(aux_dir.to_str().unwrap()),
|
||||
None)
|
||||
}
|
||||
_ => {
|
||||
let aux_dir = self.aux_output_dir_name();
|
||||
self.compose_and_run(self.make_run_args(),
|
||||
env,
|
||||
|
6
src/tools/qemu-test-client/Cargo.toml
Normal file
6
src/tools/qemu-test-client/Cargo.toml
Normal file
@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "qemu-test-client"
|
||||
version = "0.1.0"
|
||||
authors = ["The Rust Project Developers"]
|
||||
|
||||
[dependencies]
|
221
src/tools/qemu-test-client/src/main.rs
Normal file
221
src/tools/qemu-test-client/src/main.rs
Normal file
@ -0,0 +1,221 @@
|
||||
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution and at
|
||||
// http://rust-lang.org/COPYRIGHT.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
/// This is a small client program intended to pair with `qemu-test-server` in
|
||||
/// this repository. This client connects to the server over TCP and is used to
|
||||
/// push artifacts and run tests on the server instead of locally.
|
||||
///
|
||||
/// Here is also where we bake in the support to spawn the QEMU emulator as
|
||||
/// well.
|
||||
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{self, BufWriter};
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
macro_rules! t {
|
||||
($e:expr) => (match $e {
|
||||
Ok(e) => e,
|
||||
Err(e) => panic!("{} failed with {}", stringify!($e), e),
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = env::args().skip(1);
|
||||
|
||||
match &args.next().unwrap()[..] {
|
||||
"spawn-emulator" => {
|
||||
spawn_emulator(Path::new(&args.next().unwrap()),
|
||||
Path::new(&args.next().unwrap()))
|
||||
}
|
||||
"push" => {
|
||||
push(Path::new(&args.next().unwrap()))
|
||||
}
|
||||
"run" => {
|
||||
run(args.next().unwrap(), args.collect())
|
||||
}
|
||||
cmd => panic!("unknown command: {}", cmd),
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_emulator(rootfs: &Path, tmpdir: &Path) {
|
||||
// Generate a new rootfs image now that we've updated the test server
|
||||
// executable. This is the equivalent of:
|
||||
//
|
||||
// find $rootfs -print 0 | cpio --null -o --format=newc > rootfs.img
|
||||
let rootfs_img = tmpdir.join("rootfs.img");
|
||||
let mut cmd = Command::new("cpio");
|
||||
cmd.arg("--null")
|
||||
.arg("-o")
|
||||
.arg("--format=newc")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.current_dir(rootfs);
|
||||
let mut child = t!(cmd.spawn());
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
let rootfs = rootfs.to_path_buf();
|
||||
thread::spawn(move || add_files(&mut stdin, &rootfs, &rootfs));
|
||||
t!(io::copy(&mut child.stdout.take().unwrap(),
|
||||
&mut t!(File::create(&rootfs_img))));
|
||||
assert!(t!(child.wait()).success());
|
||||
|
||||
// Start up the emulator, in the background
|
||||
let mut cmd = Command::new("qemu-system-arm");
|
||||
cmd.arg("-M").arg("vexpress-a15")
|
||||
.arg("-m").arg("1024")
|
||||
.arg("-kernel").arg("/tmp/zImage")
|
||||
.arg("-initrd").arg(&rootfs_img)
|
||||
.arg("-dtb").arg("/tmp/vexpress-v2p-ca15-tc1.dtb")
|
||||
.arg("-append").arg("console=ttyAMA0 root=/dev/ram rdinit=/sbin/init init=/sbin/init")
|
||||
.arg("-nographic")
|
||||
.arg("-redir").arg("tcp:12345::12345");
|
||||
t!(cmd.spawn());
|
||||
|
||||
// Wait for the emulator to come online
|
||||
loop {
|
||||
let dur = Duration::from_millis(100);
|
||||
if let Ok(mut client) = TcpStream::connect("127.0.0.1:12345") {
|
||||
t!(client.set_read_timeout(Some(dur)));
|
||||
t!(client.set_write_timeout(Some(dur)));
|
||||
if client.write_all(b"ping").is_ok() {
|
||||
let mut b = [0; 4];
|
||||
if client.read_exact(&mut b).is_ok() {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
thread::sleep(dur);
|
||||
}
|
||||
|
||||
fn add_files(w: &mut Write, root: &Path, cur: &Path) {
|
||||
for entry in t!(cur.read_dir()) {
|
||||
let entry = t!(entry);
|
||||
let path = entry.path();
|
||||
let to_print = path.strip_prefix(root).unwrap();
|
||||
t!(write!(w, "{}\u{0}", to_print.to_str().unwrap()));
|
||||
if t!(entry.file_type()).is_dir() {
|
||||
add_files(w, root, &path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push(path: &Path) {
|
||||
let client = t!(TcpStream::connect("127.0.0.1:12345"));
|
||||
let mut client = BufWriter::new(client);
|
||||
t!(client.write_all(b"push"));
|
||||
t!(client.write_all(path.file_name().unwrap().to_str().unwrap().as_bytes()));
|
||||
t!(client.write_all(&[0]));
|
||||
let mut file = t!(File::open(path));
|
||||
t!(io::copy(&mut file, &mut client));
|
||||
t!(client.flush());
|
||||
println!("done pushing {:?}", path);
|
||||
}
|
||||
|
||||
fn run(files: String, args: Vec<String>) {
|
||||
let client = t!(TcpStream::connect("127.0.0.1:12345"));
|
||||
let mut client = BufWriter::new(client);
|
||||
t!(client.write_all(b"run "));
|
||||
|
||||
// Send over the args
|
||||
for arg in args {
|
||||
t!(client.write_all(arg.as_bytes()));
|
||||
t!(client.write_all(&[0]));
|
||||
}
|
||||
t!(client.write_all(&[0]));
|
||||
|
||||
// Send over env vars
|
||||
for (k, v) in env::vars() {
|
||||
if k != "PATH" && k != "LD_LIBRARY_PATH" {
|
||||
t!(client.write_all(k.as_bytes()));
|
||||
t!(client.write_all(&[0]));
|
||||
t!(client.write_all(v.as_bytes()));
|
||||
t!(client.write_all(&[0]));
|
||||
}
|
||||
}
|
||||
t!(client.write_all(&[0]));
|
||||
|
||||
// Send over support libraries
|
||||
let mut files = files.split(':');
|
||||
let exe = files.next().unwrap();
|
||||
for file in files.map(Path::new) {
|
||||
t!(client.write_all(file.file_name().unwrap().to_str().unwrap().as_bytes()));
|
||||
t!(client.write_all(&[0]));
|
||||
send(&file, &mut client);
|
||||
}
|
||||
t!(client.write_all(&[0]));
|
||||
|
||||
// Send over the client executable as the last piece
|
||||
send(exe.as_ref(), &mut client);
|
||||
|
||||
println!("uploaded {:?}, waiting for result", exe);
|
||||
|
||||
// Ok now it's time to read all the output. We're receiving "frames"
|
||||
// representing stdout/stderr, so we decode all that here.
|
||||
let mut header = [0; 5];
|
||||
let mut stderr_done = false;
|
||||
let mut stdout_done = false;
|
||||
let mut client = t!(client.into_inner());
|
||||
let mut stdout = io::stdout();
|
||||
let mut stderr = io::stderr();
|
||||
while !stdout_done || !stderr_done {
|
||||
t!(client.read_exact(&mut header));
|
||||
let amt = ((header[1] as u64) << 24) |
|
||||
((header[2] as u64) << 16) |
|
||||
((header[3] as u64) << 8) |
|
||||
((header[4] as u64) << 0);
|
||||
if header[0] == 0 {
|
||||
if amt == 0 {
|
||||
stdout_done = true;
|
||||
} else {
|
||||
t!(io::copy(&mut (&mut client).take(amt), &mut stdout));
|
||||
t!(stdout.flush());
|
||||
}
|
||||
} else {
|
||||
if amt == 0 {
|
||||
stderr_done = true;
|
||||
} else {
|
||||
t!(io::copy(&mut (&mut client).take(amt), &mut stderr));
|
||||
t!(stderr.flush());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, read out the exit status
|
||||
let mut status = [0; 5];
|
||||
t!(client.read_exact(&mut status));
|
||||
let code = ((status[1] as i32) << 24) |
|
||||
((status[2] as i32) << 16) |
|
||||
((status[3] as i32) << 8) |
|
||||
((status[4] as i32) << 0);
|
||||
if status[0] == 0 {
|
||||
std::process::exit(code);
|
||||
} else {
|
||||
println!("died due to signal {}", code);
|
||||
std::process::exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
fn send(path: &Path, dst: &mut Write) {
|
||||
let mut file = t!(File::open(&path));
|
||||
let amt = t!(file.metadata()).len();
|
||||
t!(dst.write_all(&[
|
||||
(amt >> 24) as u8,
|
||||
(amt >> 16) as u8,
|
||||
(amt >> 8) as u8,
|
||||
(amt >> 0) as u8,
|
||||
]));
|
||||
t!(io::copy(&mut file, dst));
|
||||
}
|
6
src/tools/qemu-test-server/Cargo.toml
Normal file
6
src/tools/qemu-test-server/Cargo.toml
Normal file
@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "qemu-test-server"
|
||||
version = "0.1.0"
|
||||
authors = ["The Rust Project Developers"]
|
||||
|
||||
[dependencies]
|
232
src/tools/qemu-test-server/src/main.rs
Normal file
232
src/tools/qemu-test-server/src/main.rs
Normal file
@ -0,0 +1,232 @@
|
||||
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution and at
|
||||
// http://rust-lang.org/COPYRIGHT.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
/// This is a small server which is intended to run inside of an emulator. This
|
||||
/// server pairs with the `qemu-test-client` program in this repository. The
|
||||
/// `qemu-test-client` connects to this server over a TCP socket and performs
|
||||
/// work such as:
|
||||
///
|
||||
/// 1. Pushing shared libraries to the server
|
||||
/// 2. Running tests through the server
|
||||
///
|
||||
/// The server supports running tests concurrently and also supports tests
|
||||
/// themselves having support libraries. All data over the TCP sockets is in a
|
||||
/// basically custom format suiting our needs.
|
||||
|
||||
use std::fs::{self, File, Permissions};
|
||||
use std::io::prelude::*;
|
||||
use std::io::{self, BufReader};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::os::unix::prelude::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::path::Path;
|
||||
use std::str;
|
||||
use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
|
||||
use std::thread;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
macro_rules! t {
|
||||
($e:expr) => (match $e {
|
||||
Ok(e) => e,
|
||||
Err(e) => panic!("{} failed with {}", stringify!($e), e),
|
||||
})
|
||||
}
|
||||
|
||||
static TEST: AtomicUsize = ATOMIC_USIZE_INIT;
|
||||
|
||||
fn main() {
|
||||
println!("starting test server");
|
||||
let listener = t!(TcpListener::bind("10.0.2.15:12345"));
|
||||
println!("listening!");
|
||||
|
||||
let work = Path::new("/tmp/work");
|
||||
t!(fs::create_dir_all(work));
|
||||
|
||||
let lock = Arc::new(Mutex::new(()));
|
||||
|
||||
for socket in listener.incoming() {
|
||||
let mut socket = t!(socket);
|
||||
let mut buf = [0; 4];
|
||||
t!(socket.read_exact(&mut buf));
|
||||
if &buf[..] == b"ping" {
|
||||
t!(socket.write_all(b"pong"));
|
||||
} else if &buf[..] == b"push" {
|
||||
handle_push(socket, work);
|
||||
} else if &buf[..] == b"run " {
|
||||
let lock = lock.clone();
|
||||
thread::spawn(move || handle_run(socket, work, &lock));
|
||||
} else {
|
||||
panic!("unknown command {:?}", buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_push(socket: TcpStream, work: &Path) {
|
||||
let mut reader = BufReader::new(socket);
|
||||
let mut filename = Vec::new();
|
||||
t!(reader.read_until(0, &mut filename));
|
||||
filename.pop(); // chop off the 0
|
||||
let filename = t!(str::from_utf8(&filename));
|
||||
|
||||
let path = work.join(filename);
|
||||
t!(io::copy(&mut reader, &mut t!(File::create(&path))));
|
||||
t!(fs::set_permissions(&path, Permissions::from_mode(0o755)));
|
||||
}
|
||||
|
||||
struct RemoveOnDrop<'a> {
|
||||
inner: &'a Path,
|
||||
}
|
||||
|
||||
impl<'a> Drop for RemoveOnDrop<'a> {
|
||||
fn drop(&mut self) {
|
||||
t!(fs::remove_dir_all(self.inner));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_run(socket: TcpStream, work: &Path, lock: &Mutex<()>) {
|
||||
let mut arg = Vec::new();
|
||||
let mut reader = BufReader::new(socket);
|
||||
|
||||
// Allocate ourselves a directory that we'll delete when we're done to save
|
||||
// space.
|
||||
let n = TEST.fetch_add(1, Ordering::SeqCst);
|
||||
let path = work.join(format!("test{}", n));
|
||||
let exe = path.join("exe");
|
||||
t!(fs::create_dir(&path));
|
||||
let _a = RemoveOnDrop { inner: &path };
|
||||
|
||||
// First up we'll get a list of arguments delimited with 0 bytes. An empty
|
||||
// argument means that we're done.
|
||||
let mut cmd = Command::new(&exe);
|
||||
while t!(reader.read_until(0, &mut arg)) > 1 {
|
||||
cmd.arg(t!(str::from_utf8(&arg[..arg.len() - 1])));
|
||||
arg.truncate(0);
|
||||
}
|
||||
|
||||
// Next we'll get a bunch of env vars in pairs delimited by 0s as well
|
||||
arg.truncate(0);
|
||||
while t!(reader.read_until(0, &mut arg)) > 1 {
|
||||
let key_len = arg.len() - 1;
|
||||
let val_len = t!(reader.read_until(0, &mut arg)) - 1;
|
||||
{
|
||||
let key = &arg[..key_len];
|
||||
let val = &arg[key_len + 1..][..val_len];
|
||||
let key = t!(str::from_utf8(key));
|
||||
let val = t!(str::from_utf8(val));
|
||||
cmd.env(key, val);
|
||||
}
|
||||
arg.truncate(0);
|
||||
}
|
||||
|
||||
// The section of code from here down to where we drop the lock is going to
|
||||
// be a critical section for us. On Linux you can't execute a file which is
|
||||
// open somewhere for writing, as you'll receive the error "text file busy".
|
||||
// Now here we never have the text file open for writing when we spawn it,
|
||||
// so why do we still need a critical section?
|
||||
//
|
||||
// Process spawning first involves a `fork` on Unix, which clones all file
|
||||
// descriptors into the child process. This means that it's possible for us
|
||||
// to open the file for writing (as we're downloading it), then some other
|
||||
// thread forks, then we close the file and try to exec. At that point the
|
||||
// other thread created a child process with the file open for writing, and
|
||||
// we attempt to execute it, so we get an error.
|
||||
//
|
||||
// This race is resolve by ensuring that only one thread can writ ethe file
|
||||
// and spawn a child process at once. Kinda an unfortunate solution, but we
|
||||
// don't have many other choices with this sort of setup!
|
||||
//
|
||||
// In any case the lock is acquired here, before we start writing any files.
|
||||
// It's then dropped just after we spawn the child. That way we don't lock
|
||||
// the execution of the child, just the creation of its files.
|
||||
let lock = lock.lock();
|
||||
|
||||
// Next there's a list of dynamic libraries preceded by their filenames.
|
||||
arg.truncate(0);
|
||||
while t!(reader.read_until(0, &mut arg)) > 1 {
|
||||
let dst = path.join(t!(str::from_utf8(&arg[..arg.len() - 1])));
|
||||
let amt = read_u32(&mut reader) as u64;
|
||||
t!(io::copy(&mut reader.by_ref().take(amt),
|
||||
&mut t!(File::create(&dst))));
|
||||
t!(fs::set_permissions(&dst, Permissions::from_mode(0o755)));
|
||||
arg.truncate(0);
|
||||
}
|
||||
|
||||
// Finally we'll get the binary. The other end will tell us how big the
|
||||
// binary is and then we'll download it all to the exe path we calculated
|
||||
// earlier.
|
||||
let amt = read_u32(&mut reader) as u64;
|
||||
t!(io::copy(&mut reader.by_ref().take(amt),
|
||||
&mut t!(File::create(&exe))));
|
||||
t!(fs::set_permissions(&exe, Permissions::from_mode(0o755)));
|
||||
|
||||
// Support libraries were uploaded to `work` earlier, so make sure that's
|
||||
// in `LD_LIBRARY_PATH`. Also include our own current dir which may have
|
||||
// had some libs uploaded.
|
||||
cmd.env("LD_LIBRARY_PATH",
|
||||
format!("{}:{}", work.display(), path.display()));
|
||||
|
||||
// Spawn the child and ferry over stdout/stderr to the socket in a framed
|
||||
// fashion (poor man's style)
|
||||
let mut child = t!(cmd.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn());
|
||||
drop(lock);
|
||||
let mut stdout = child.stdout.take().unwrap();
|
||||
let mut stderr = child.stderr.take().unwrap();
|
||||
let socket = Arc::new(Mutex::new(reader.into_inner()));
|
||||
let socket2 = socket.clone();
|
||||
let thread = thread::spawn(move || my_copy(&mut stdout, 0, &*socket2));
|
||||
my_copy(&mut stderr, 1, &*socket);
|
||||
thread.join().unwrap();
|
||||
|
||||
// Finally send over the exit status.
|
||||
let status = t!(child.wait());
|
||||
let (which, code) = match status.code() {
|
||||
Some(n) => (0, n),
|
||||
None => (1, status.signal().unwrap()),
|
||||
};
|
||||
t!(socket.lock().unwrap().write_all(&[
|
||||
which,
|
||||
(code >> 24) as u8,
|
||||
(code >> 16) as u8,
|
||||
(code >> 8) as u8,
|
||||
(code >> 0) as u8,
|
||||
]));
|
||||
}
|
||||
|
||||
fn my_copy(src: &mut Read, which: u8, dst: &Mutex<Write>) {
|
||||
let mut b = [0; 1024];
|
||||
loop {
|
||||
let n = t!(src.read(&mut b));
|
||||
let mut dst = dst.lock().unwrap();
|
||||
t!(dst.write_all(&[
|
||||
which,
|
||||
(n >> 24) as u8,
|
||||
(n >> 16) as u8,
|
||||
(n >> 8) as u8,
|
||||
(n >> 0) as u8,
|
||||
]));
|
||||
if n > 0 {
|
||||
t!(dst.write_all(&b[..n]));
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_u32(r: &mut Read) -> u32 {
|
||||
let mut len = [0; 4];
|
||||
t!(r.read_exact(&mut len));
|
||||
((len[0] as u32) << 24) |
|
||||
((len[1] as u32) << 16) |
|
||||
((len[2] as u32) << 8) |
|
||||
((len[3] as u32) << 0)
|
||||
}
|
Loading…
Reference in New Issue
Block a user