Initial commit
This commit is contained in:
parent
c33ff84411
commit
5bd9bdfa35
2
Cargo.toml
Normal file
2
Cargo.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[workspace]
|
||||
members = ["warp-runner", "warp-packer"]
|
12
warp-packer/Cargo.toml
Normal file
12
warp-packer/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "warp-packer"
|
||||
version = "0.1.0"
|
||||
authors = ["Diego Giagio <diego@giagio.com>"]
|
||||
|
||||
[dependencies]
|
||||
clap = "2.32.0"
|
||||
dirs = "1.0.4"
|
||||
reqwest = "0.9.2"
|
||||
tempdir = "0.3.7"
|
||||
flate2 = "1.0"
|
||||
tar = "0.4"
|
219
warp-packer/src/main.rs
Normal file
219
warp-packer/src/main.rs
Normal file
@ -0,0 +1,219 @@
|
||||
extern crate clap;
|
||||
extern crate dirs;
|
||||
extern crate reqwest;
|
||||
extern crate tempdir;
|
||||
extern crate tar;
|
||||
extern crate flate2;
|
||||
|
||||
use clap::{App, AppSettings, Arg};
|
||||
use std::process;
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
use std::io::copy;
|
||||
use tempdir::TempDir;
|
||||
use std::path::Path;
|
||||
use std::io;
|
||||
use std::error::Error;
|
||||
use std::io::Write;
|
||||
use std::io::Read;
|
||||
use std::fs::Metadata;
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
|
||||
const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const AUTHOR: &str = env!("CARGO_PKG_AUTHORS");
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
const SUPPORTED_ARCHS: &[&str] = &["linux-x64", "windows-x64", "macos-x64"];
|
||||
const RUNNER_URL_TEMPLATE: &str = "https://s3.amazonaws.com/dgiagio-public/warp/$VERSION$/$ARCH$/warp-runner";
|
||||
const RUNNER_MAGIC: &[u8] = b"tVQhhsFFlGGD3oWV4lEPST8I8FEPP54IM0q7daes4E1y3p2U2wlJRYmWmjPYfkhZ0PlT14Ls0j8fdDkoj33f2BlRJavLj3mWGibJsGt5uLAtrCDtvxikZ8UX2mQDCrgE\0";
|
||||
|
||||
/// Print a message to stderr and exit with error code 1
|
||||
macro_rules! bail {
|
||||
() => (process::exit(1));
|
||||
($($arg:tt)*) => ({
|
||||
eprint!("{}\n", format_args!($($arg)*));
|
||||
process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
fn runners_dir() -> PathBuf {
|
||||
dirs::data_local_dir()
|
||||
.expect("No data local dir found")
|
||||
.join("warp")
|
||||
.join("runners")
|
||||
}
|
||||
|
||||
fn runner_url(arch: &str) -> String {
|
||||
let mut ext = "";
|
||||
if cfg!(target_family = "windows") {
|
||||
ext = ".exe";
|
||||
}
|
||||
|
||||
RUNNER_URL_TEMPLATE
|
||||
.replace("$VERSION$", VERSION)
|
||||
.replace("$ARCH$", arch) + ext
|
||||
}
|
||||
|
||||
fn patch_runner(runner_exec: &Path, new_runner_exec: &Path, exec_name: &str) -> io::Result<()> {
|
||||
// Read runner executable in memory
|
||||
let mut buf = vec![];
|
||||
fs::File::open(runner_exec)?
|
||||
.read_to_end(&mut buf)?;
|
||||
|
||||
// Set the correct target executable name into the local magic buffer
|
||||
let magic_len = RUNNER_MAGIC.len();
|
||||
let mut new_magic = vec![0; magic_len];
|
||||
new_magic[..exec_name.len()].clone_from_slice(exec_name.as_bytes());
|
||||
|
||||
// Find the magic buffer offset inside the runner executable
|
||||
let mut offs_opt = None;
|
||||
for (i, chunk) in buf.windows(magic_len).enumerate() {
|
||||
if chunk == RUNNER_MAGIC {
|
||||
offs_opt = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if offs_opt.is_none() {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "no magic found inside runner"))
|
||||
}
|
||||
|
||||
// Replace the magic with the new one that points to the target executable
|
||||
let offs = offs_opt.unwrap();
|
||||
buf[offs..offs + magic_len].clone_from_slice(&new_magic);
|
||||
|
||||
// Write patched runner to disk
|
||||
fs::File::create(&new_runner_exec)?
|
||||
.write_all(&buf)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
fn is_executable(path: &Path, _: &Metadata) -> bool {
|
||||
if let Some(ext) = path.extension() {
|
||||
ext == "exe"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
fn is_executable(_: &Path, metadata: &Metadata) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
const S_IXUSR: u32 = 0o100;
|
||||
return metadata.permissions().mode() & S_IXUSR == S_IXUSR
|
||||
}
|
||||
|
||||
fn create_tgz(dir: &Path, out: &Path) -> io::Result<()> {
|
||||
let f = fs::File::create(out)?;
|
||||
let gz = GzEncoder::new(f, Compression::best());
|
||||
let mut tar = tar::Builder::new(gz);
|
||||
tar.append_dir_all(".", dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_app(runner_exec: &Path, tgz_path: &Path, out: &Path) -> io::Result<()> {
|
||||
let mut outf = fs::File::create(out)?;
|
||||
let mut runnerf = fs::File::open(runner_exec)?;
|
||||
let mut tgzf = fs::File::open(tgz_path)?;
|
||||
copy(&mut runnerf, &mut outf)?;
|
||||
copy(&mut tgzf, &mut outf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<Error>> {
|
||||
let args = App::new(APP_NAME)
|
||||
.settings(&[AppSettings::ArgRequiredElseHelp, AppSettings::ColoredHelp])
|
||||
.version(VERSION)
|
||||
.author(AUTHOR)
|
||||
.about("Create self-contained single binary application")
|
||||
.arg(Arg::with_name("arch")
|
||||
.short("a")
|
||||
.long("arch")
|
||||
.value_name("arch")
|
||||
.help(&format!("Sets the architecture. Supported: {:?}", SUPPORTED_ARCHS))
|
||||
.display_order(1)
|
||||
.takes_value(true)
|
||||
.required(true))
|
||||
.arg(Arg::with_name("input_dir")
|
||||
.short("i")
|
||||
.long("input_dir")
|
||||
.value_name("input_dir")
|
||||
.help("Sets the input directory containing the application and dependencies")
|
||||
.display_order(2)
|
||||
.takes_value(true)
|
||||
.required(true))
|
||||
.arg(Arg::with_name("exec")
|
||||
.short("e")
|
||||
.long("exec")
|
||||
.value_name("exec")
|
||||
.help("Sets the application executable file name")
|
||||
.display_order(3)
|
||||
.takes_value(true)
|
||||
.required(true))
|
||||
.arg(Arg::with_name("output")
|
||||
.short("o")
|
||||
.long("output")
|
||||
.value_name("output")
|
||||
.help("Sets the resulting self-contained application file name")
|
||||
.display_order(4)
|
||||
.takes_value(true)
|
||||
.required(true))
|
||||
.get_matches();
|
||||
|
||||
let arch = args.value_of("arch").unwrap();
|
||||
if !SUPPORTED_ARCHS.contains(&arch) {
|
||||
bail!("Unknown architecture specified: {}, supported: {:?}", arch, SUPPORTED_ARCHS);
|
||||
}
|
||||
|
||||
let input_dir = Path::new(args.value_of("input_dir").unwrap());
|
||||
if fs::metadata(input_dir).is_err() {
|
||||
bail!("Cannot access specified input directory {:?}", input_dir);
|
||||
}
|
||||
|
||||
let exec_name = args.value_of("exec").unwrap();
|
||||
if exec_name.len() >= RUNNER_MAGIC.len() {
|
||||
bail!("Executable name is too long, please consider using a shorter name");
|
||||
}
|
||||
|
||||
let exec_path = Path::new(input_dir).join(exec_name);
|
||||
match fs::metadata(&exec_path) {
|
||||
Err(_) => {
|
||||
bail!("Cannot find executable {} inside directory {:?}", exec_name, input_dir);
|
||||
},
|
||||
Ok(metadata) => {
|
||||
if !is_executable(&exec_path, &metadata) {
|
||||
bail!("File {} inside directory {:?} isn't executable", exec_name, input_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let runners_dir = runners_dir();
|
||||
fs::create_dir_all(&runners_dir)?;
|
||||
|
||||
let runner_exec = runners_dir.join(arch);
|
||||
if !runner_exec.exists() {
|
||||
let url = runner_url(arch);
|
||||
println!("Downloading runner from {}...", url);
|
||||
let mut response = reqwest::get(&url)?.error_for_status()?;
|
||||
let mut f = fs::File::create(&runner_exec)?;
|
||||
copy(&mut response, &mut f)?;
|
||||
}
|
||||
|
||||
let tmp_dir = TempDir::new(APP_NAME)?;
|
||||
let new_runner_exec = tmp_dir.path().join("runner");
|
||||
patch_runner(&runner_exec, &new_runner_exec, &exec_name)?;
|
||||
|
||||
println!("Compressing input directory {:?}...", input_dir);
|
||||
let tgz_path = tmp_dir.path().join("input.tgz");
|
||||
create_tgz(&input_dir, &tgz_path)?;
|
||||
|
||||
let exec_name = Path::new(args.value_of("output").unwrap());
|
||||
println!("Creating self-contained application binary {:?}...", exec_name);
|
||||
create_app(&new_runner_exec, &tgz_path, &exec_name)?;
|
||||
|
||||
println!("All done");
|
||||
Ok(())
|
||||
}
|
13
warp-runner/Cargo.toml
Normal file
13
warp-runner/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "warp-runner"
|
||||
version = "0.1.0"
|
||||
authors = ["Diego"]
|
||||
|
||||
[dependencies]
|
||||
memmem = "0.1.1"
|
||||
flate2 = "1.0"
|
||||
tar = "0.4"
|
||||
dirs = "1.0.4"
|
||||
winapi = "0.3.6"
|
||||
log = "0.4.5"
|
||||
simple_logger = "1.0.1"
|
33
warp-runner/src/executor.rs
Normal file
33
warp-runner/src/executor.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
const PATH_SEPARATOR: char = ';';
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
const PATH_SEPARATOR: char = ':';
|
||||
|
||||
pub fn execute(path: &Path, prog: &str) {
|
||||
let path_str = path.as_os_str().to_os_string().into_string().unwrap();
|
||||
let path_env = match env::var("PATH") {
|
||||
Ok(p) => format!("{}{}{}", &path_str, PATH_SEPARATOR, &p),
|
||||
_ => path_str
|
||||
};
|
||||
|
||||
let mut args: Vec<String> = env::args().collect();
|
||||
args[0] = prog.to_owned();
|
||||
|
||||
trace!("PATH={:?} prog={:?} args={:?}", path_env, prog, args);
|
||||
Command::new(prog)
|
||||
.env("PATH", path_env)
|
||||
.args(args)
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.unwrap_or_else(|_| panic!("{} failed to start", prog))
|
||||
.wait()
|
||||
.unwrap_or_else(|_| panic!("{} failed to wait", prog));
|
||||
}
|
99
warp-runner/src/extractor.rs
Normal file
99
warp-runner/src/extractor.rs
Normal file
@ -0,0 +1,99 @@
|
||||
extern crate flate2;
|
||||
extern crate memmem;
|
||||
extern crate tar;
|
||||
|
||||
use self::flate2::read::GzDecoder;
|
||||
use self::memmem::{Searcher, TwoWaySearcher};
|
||||
use self::tar::Archive;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::{BufReader, Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
struct FileSearcher<'a> {
|
||||
buf_reader: BufReader<File>,
|
||||
searcher: TwoWaySearcher<'a>,
|
||||
offs: usize,
|
||||
}
|
||||
|
||||
impl<'a> FileSearcher<'a> {
|
||||
fn new(path: &'a Path, magic: &'a [u8]) -> io::Result<FileSearcher<'a>> {
|
||||
let file = File::open(path)?;
|
||||
Ok(FileSearcher {
|
||||
buf_reader: BufReader::new(file),
|
||||
searcher: TwoWaySearcher::new(magic),
|
||||
offs: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FileSearcher<'a> {
|
||||
type Item = io::Result<usize>;
|
||||
|
||||
fn next(&mut self) -> Option<io::Result<usize>> {
|
||||
let mut buf = [0; 32 * 1024];
|
||||
let ret;
|
||||
|
||||
match self.buf_reader.seek(SeekFrom::Start(self.offs as u64)) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Some(Err(e))
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.buf_reader.read(&mut buf[..]) {
|
||||
Ok(0) => {
|
||||
ret = None;
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
match self.searcher.search_in(&buf) {
|
||||
Some(pos) => {
|
||||
self.offs += pos;
|
||||
ret = Some(Ok(self.offs));
|
||||
self.offs += 1; // one past the match so we can try again if necessary
|
||||
break;
|
||||
}
|
||||
None => self.offs += n
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ret = Some(Err(e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
const GZIP_MAGIC: &[u8] = b"\x1f\x8b\x08";
|
||||
|
||||
pub fn extract_to(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
let mut found = false;
|
||||
|
||||
let searcher = FileSearcher::new(src, GZIP_MAGIC)?;
|
||||
for result in searcher {
|
||||
let offs = result?;
|
||||
if extract_at_offset(src, offs, dst).is_ok() {
|
||||
trace!("tarball found at offset {} was extracted successfully", offs);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "no tarball found inside binary"))
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_at_offset(src: &Path, offs: usize, dst: &Path) -> io::Result<()> {
|
||||
let mut f = File::open(src)?;
|
||||
f.seek(SeekFrom::Start(offs as u64))?;
|
||||
|
||||
let gz = GzDecoder::new(f);
|
||||
let mut tar = Archive::new(gz);
|
||||
tar.unpack(dst)?;
|
||||
Ok(())
|
||||
}
|
73
warp-runner/src/main.rs
Normal file
73
warp-runner/src/main.rs
Normal file
@ -0,0 +1,73 @@
|
||||
extern crate dirs;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate simple_logger;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::ffi::*;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::*;
|
||||
use log::Level;
|
||||
|
||||
mod extractor;
|
||||
mod executor;
|
||||
|
||||
static PROG_BUF: &'static [u8] = b"tVQhhsFFlGGD3oWV4lEPST8I8FEPP54IM0q7daes4E1y3p2U2wlJRYmWmjPYfkhZ0PlT14Ls0j8fdDkoj33f2BlRJavLj3mWGibJsGt5uLAtrCDtvxikZ8UX2mQDCrgE\0";
|
||||
|
||||
fn prog() -> &'static str {
|
||||
let nul_pos = PROG_BUF.iter()
|
||||
.position(|elem| *elem == b'\0')
|
||||
.expect("PROG_BUF has no NUL terminator");
|
||||
|
||||
let slice = &PROG_BUF[..(nul_pos + 1)];
|
||||
CStr::from_bytes_with_nul(slice)
|
||||
.expect("Can't convert PROG_BUF slice to CStr")
|
||||
.to_str()
|
||||
.expect("Can't convert PROG_BUF CStr to str")
|
||||
}
|
||||
|
||||
fn cache_path(prog: &str) -> PathBuf {
|
||||
dirs::data_local_dir()
|
||||
.expect("No data local dir found")
|
||||
.join("warp")
|
||||
.join("packages")
|
||||
.join(prog)
|
||||
}
|
||||
|
||||
fn extract(exe_path: &Path, cache_path: &Path) -> io::Result<()> {
|
||||
fs::remove_dir_all(cache_path).ok();
|
||||
extractor::extract_to(&exe_path, &cache_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<Error>> {
|
||||
if env::var("WARP_TRACE").is_ok() {
|
||||
simple_logger::init_with_level(Level::Trace)?;
|
||||
}
|
||||
|
||||
let prog = prog();
|
||||
let cache_path = cache_path(prog);
|
||||
let exe_path = env::current_exe()?;
|
||||
trace!("prog={:?}, cache_path={:?}, exe_path={:?}", prog, cache_path, exe_path);
|
||||
|
||||
match fs::metadata(&cache_path) {
|
||||
Ok(cache) => {
|
||||
if cache.modified()? >= fs::metadata(&exe_path)?.modified()? {
|
||||
trace!("cache is up-to-date");
|
||||
} else {
|
||||
trace!("cache is outdated");
|
||||
extract(&exe_path, &cache_path)?;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
trace!("cache not found");
|
||||
extract(&exe_path, &cache_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
executor::execute(&cache_path, &prog);
|
||||
Ok(())
|
||||
}
|
25
warp.iml
Normal file
25
warp.iml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="RUST_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/warp-packer/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/warp-packer/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/warp-packer/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/warp-packer/benches" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/warp-runner/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/warp-runner/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/warp-runner/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/warp-runner/benches" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/warp-packer/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/warp-runner/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
Loading…
Reference in New Issue
Block a user