Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions tools/cargo-run/internal/download/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "cargo-run-internal-download"
edition.workspace = true
version.workspace = true
license.workspace = true
authors.workspace = true

[dependencies]
cargo-run = { path = "../.." }
ureq = { version = "3", default-features = false, features = ["rustls"] }
sha2 = { version = "0.10" }
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
tar = { version = "0.4", default-features = false }
163 changes: 163 additions & 0 deletions tools/cargo-run/internal/download/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use cargo_run::Error;
use flate2::read::GzDecoder;
use sha2::{Digest, Sha256};
use std::io::{self, Read};
use std::path::{Component, Path, PathBuf};
use std::process::ExitCode;

pub fn usage() {
eprintln!();
eprintln!("USAGE:");
eprintln!(" cargo run -p cargo-run-internal-download -- <URL> <SHA256> <OUT> \\");
eprintln!(" [--extract] [--strip <N>] [--include <PREFIX>]...");
eprintln!();
eprintln!("Args:");
eprintln!(" <URL> HTTPS source to download");
eprintln!(" <SHA256> Expected SHA-256 of the response body (64 hex digits)");
eprintln!(" <OUT> Destination file (default) or directory (with --extract)");
eprintln!(" --extract Decompress the body as tar.gz and extract into <OUT>");
eprintln!(" --strip <N> Strip N leading path components from each entry (requires --extract)");
eprintln!(" --include <PREFIX> Only extract entries whose stripped path starts with PREFIX, repeatable (requires --extract)");
eprintln!();
}

fn main() -> ExitCode {
if let Err(e) = parse_args().inspect_err(|_| usage()).and_then(run) {
eprintln!("Error: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}

struct Args {
url: String,
sha256: String,
out: PathBuf,
mode: Mode,
}

enum Mode {
File,
ExtractTarGz { strip: usize, include: Vec<String> },
}

fn run(Args { url, sha256, out, mode }: Args) -> Result<(), Error> {
eprintln!("Downloading {url}");
let mut body = Vec::new();
ureq::get(&url)
.call()
.map_err(|e| Error::Io(io::Error::other(e), format!("HTTP GET {url}")))?
.into_body()
.into_with_config()
.reader()
.read_to_end(&mut body)
.map_err(|e| Error::Io(io::Error::other(e), "reading response body".into()))?;

let actual = Sha256::digest(&body).iter().map(|b| format!("{b:02x}")).collect::<String>();
if !actual.eq_ignore_ascii_case(&sha256) {
eprintln!("SHA-256 mismatch:");
eprintln!(" expected: {sha256}");
eprintln!(" actual: {actual}");
return Err(Error::Io(io::Error::other("SHA-256 mismatch"), "verifying download".into()));
}

match mode {
Mode::File => write_to_file(&body, &out),
Mode::ExtractTarGz { strip, include } => extract_tar_gz(&body, &out, strip, &include),
}
}

fn write_to_file(body: &[u8], path: &Path) -> Result<(), Error> {
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent).map_err(|e| Error::Io(e, format!("creating '{}'", parent.display())))?;
}
std::fs::write(path, body).map_err(|e| Error::Io(e, format!("writing '{}'", path.display())))?;
eprintln!("Wrote {}", path.display());
Ok(())
}

// TODO: support other compression/archive formats
fn extract_tar_gz(body: &[u8], dir: &Path, strip: usize, include: &[String]) -> Result<(), Error> {
std::fs::create_dir_all(dir).map_err(|e| Error::Io(e, format!("creating '{}'", dir.display())))?;
let mut archive = tar::Archive::new(GzDecoder::new(body));
archive.set_preserve_permissions(true);
for entry in archive.entries().map_err(|e| Error::Io(e, "reading tar archive".into()))? {
let mut entry = entry.map_err(|e| Error::Io(e, "reading tar entry".into()))?;
let entry_path = entry.path().map_err(|e| Error::Io(e, "reading tar entry path".into()))?.into_owned();
let Some(stripped) = strip_components(&entry_path, strip) else {
continue;
};
if stripped.as_os_str().is_empty() || stripped.components().any(|c| matches!(c, Component::ParentDir | Component::Prefix(_) | Component::RootDir)) {
continue;
}
if !include.is_empty() {
let s = stripped.to_string_lossy().replace('\\', "/");
if !include.iter().any(|p| s.starts_with(p.as_str())) {
continue;
}
}
let entry_type = entry.header().entry_type();
if entry_type.is_symlink() || entry_type.is_hard_link() {
continue;
}
let target = dir.join(&stripped);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).map_err(|e| Error::Io(e, format!("creating '{}'", parent.display())))?;
}
entry.unpack(&target).map_err(|e| Error::Io(e, format!("unpacking '{}'", target.display())))?;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}
eprintln!("Extracted into {}", dir.display());
Ok(())
}

fn strip_components(path: &Path, n: usize) -> Option<PathBuf> {
let mut comps = path.components();
for _ in 0..n {
comps.next()?;
}
Some(comps.as_path().to_path_buf())
}

fn parse_args() -> Result<Args, Error> {
fn arg_err(msg: impl Into<String>) -> Error {
Error::Io(io::Error::new(io::ErrorKind::InvalidInput, msg.into()), "invalid arguments".into())
}

let mut args = std::env::args().skip(1);
let url = args.next().ok_or_else(|| arg_err("URL is required (first positional argument)"))?;
let sha256 = args.next().ok_or_else(|| arg_err("SHA-256 is required (second positional argument)"))?;
if sha256.len() != 64 || !sha256.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(arg_err("SHA-256 must be 64 hex digits"));
}
let out = PathBuf::from(args.next().ok_or_else(|| arg_err("OUT is required (third positional argument)"))?);

let mut extract = false;
let mut strip: usize = 0;
let mut include: Vec<String> = Vec::new();

while let Some(arg) = args.next() {
match arg.as_str() {
"--extract" => extract = true,
"--strip" => {
let v = args.next().ok_or_else(|| arg_err("'--strip' requires a value"))?;
strip = v.parse().map_err(|_| arg_err(format!("--strip must be a non-negative integer, got '{v}'")))?;
}
"--include" => include.push(args.next().ok_or_else(|| arg_err("'--include' requires a value"))?),
other => return Err(arg_err(format!("unknown flag '{other}'"))),
}
}

let mode = if extract {
Mode::ExtractTarGz { strip, include }
} else {
if strip != 0 {
return Err(arg_err("--strip is only valid with --extract"));
}
if !include.is_empty() {
return Err(arg_err("--include is only valid with --extract"));
}
Mode::File
};

Ok(Args { url, sha256, out, mode })
}
7 changes: 3 additions & 4 deletions tools/cargo-run/internal/watch/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::path::Path;
use std::process::ExitCode;
use std::time::Duration;

const EXCLUDED_DIRECTORIES: &[&str] = &["target", ".git", "frontend/node_modules", "frontend/dist", "frontend/wrapper/pkg"];
const EXCLUDED_DIRECTORIES: &[&str] = &["target", ".git", "frontend/node_modules", "frontend/dist", "frontend/wrapper/pkg", "tools"];
const INCLUDED_EXTENSIONS: &[&str] = &["rs"];

const DEBOUNCE: Duration = Duration::from_millis(500);
Expand All @@ -23,17 +23,16 @@ fn main() -> ExitCode {
return ExitCode::FAILURE;
}
};
println!("Watching for changes...");
loop {
std::thread::park();
}
}

pub struct WatchGuard {
struct WatchGuard {
_debouncer: Debouncer<RecommendedWatcher, RecommendedCache>,
}

pub fn watch(steps: impl IntoIterator<Item = Expression>) -> Result<WatchGuard, Error> {
fn watch(steps: impl IntoIterator<Item = Expression>) -> Result<WatchGuard, Error> {
let steps: Vec<Expression> = steps.into_iter().collect();
let root = std::env::current_dir()
.and_then(|p| p.canonicalize())
Expand Down
16 changes: 16 additions & 0 deletions tools/cargo-run/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ pub enum TerminalColor {
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Reset,
}
impl TerminalColor {
Expand All @@ -97,6 +105,14 @@ impl TerminalColor {
Self::Magenta => "\x1b[35m",
Self::Cyan => "\x1b[36m",
Self::White => "\x1b[37m",
Self::BrightBlack => "\x1b[90m",
Self::BrightRed => "\x1b[91m",
Self::BrightGreen => "\x1b[92m",
Self::BrightYellow => "\x1b[93m",
Self::BrightBlue => "\x1b[94m",
Self::BrightMagenta => "\x1b[95m",
Self::BrightCyan => "\x1b[96m",
Self::BrightWhite => "\x1b[97m",
Self::Reset => "\x1b[0m",
}
}
Expand Down
8 changes: 8 additions & 0 deletions tools/cargo-run/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ pub fn target_dir() -> PathBuf {
}
}

pub fn install_dir() -> PathBuf {
target_dir().join("cargo-run")
}

pub fn bin_dir() -> PathBuf {
install_dir().join("bin")
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("One or more requirements were not met")]
Expand Down
2 changes: 1 addition & 1 deletion tools/cargo-run/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ fn run_task(task: &Task) -> Result<(), Error> {
}

fn prepend_path() {
let mut paths = vec![target_dir().join("cargo-run").join("bin")];
let mut paths = vec![bin_dir()];
if let Some(path) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&path));
}
Expand Down
Loading
Loading