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
34 changes: 8 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Install Rust toolchain
run: |
Expand All @@ -34,15 +34,15 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Install Rust toolchain
run: |
rustup toolchain install stable --profile minimal --target x86_64-unknown-linux-gnu
rustup default stable

- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"

Expand Down Expand Up @@ -70,58 +70,40 @@ jobs:
os: windows-2025
- name: macOS x64
os: macos-15-intel
- name: macOS arm64
os: macos-15
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Install Rust toolchain
run: |
rustup toolchain install stable --profile minimal
rustup default stable

- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"

- name: Test release tooling
run: python -m unittest discover -s tests/xpy -v

- name: Run unit and integration tests
if: runner.os != 'Windows'
run: cargo test --locked

- name: Run unit tests
if: runner.os == 'Windows'
run: cargo test --locked --bin vex

- name: Build
run: cargo build --locked

macos-arm64:
name: Build / macOS arm64
runs-on: macos-15-intel
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Install Rust toolchain
run: |
rustup toolchain install stable --profile minimal --target aarch64-apple-darwin
rustup default stable

- name: Cross-build
run: cargo build --locked --target aarch64-apple-darwin

linux-riscv64:
name: Build / Linux riscv64
runs-on: ubuntu-24.04
env:
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Install RISC-V system tools
run: |
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ Add tests at the same level as the behavior being changed:
- compiler invocation changes require dry-run schema and end-to-end smoke tests
- release-tool changes require `python3 -m unittest discover -s tests/xpy -v`
- package changes require archive-content, checksum, and executable smoke tests
- platform changes must preserve native integration coverage where a hosted
runner exists; cross-build-only targets must be documented as experimental

Git integration tests must use local fixture repositories and must not require
external network access. Dependency changes should cover direct and transitive
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ Vex is designed to sit above `wavec` in the same way Cargo sits above `rustc`: V

Vex runs `wavec` from `PATH` by default. Set `VEX_WAVEC=/path/to/wavec` to use a specific compiler binary.

## Platform validation

The v0.0.1 release candidate targets below are validated on every pull request.
“Candidate” means the platform is intended to receive a release archive after
the release workflow also passes package and clean-environment smoke tests.

| Platform | Rust target | CI validation | v0.0.1 status |
| --- | --- | --- | --- |
| Linux amd64 | `x86_64-unknown-linux-gnu` | native tests, build, package smoke | Candidate |
| Linux arm64 | `aarch64-unknown-linux-gnu` | native tests and build | Candidate |
| Windows x64 | `x86_64-pc-windows-msvc` | native tests and build | Candidate |
| macOS Intel | `x86_64-apple-darwin` | native tests and build | Candidate |
| macOS Apple Silicon | `aarch64-apple-darwin` | native tests and build | Candidate |
| Linux RISC-V | `riscv64gc-unknown-linux-gnu` | cross-build and QEMU version smoke | Experimental |

Windows release artifacts use the MSVC target. A Windows GNU artifact is not
part of the v0.0.1 scope. RISC-V remains experimental because its test coverage
is limited to cross-build and QEMU smoke rather than the complete integration
suite. Final minimum OS and glibc versions will be fixed by the release workflow
before v0.0.1 is tagged.

## Commands

```sh
Expand Down
106 changes: 73 additions & 33 deletions src/resolver.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -452,11 +453,7 @@ impl Resolver<'_> {
"refs/remotes/origin/HEAD^{commit}".to_string()
};
let commit = git_stdout(
Command::new("git").arg("-C").arg(destination).args([
"rev-parse",
"--verify",
&reference,
]),
git_command_in(destination).args(["rev-parse", "--verify", &reference]),
"resolve Git dependency reference",
)?;
checkout_commit(destination, &commit)?;
Expand Down Expand Up @@ -507,8 +504,11 @@ fn ensure_repository(destination: &Path, url: &str, name: &str) -> Result<(), St
fs::create_dir_all(parent)
.map_err(|e| format!("failed to create `{}`: {e}", parent.display()))?;
ui::status("Cloning", format!("{name} ({url})"));
let destination = git_cli_path(destination);
run_git(
Command::new("git").args(["clone", url]).arg(destination),
Command::new("git")
.args(["clone", url])
.arg(destination.as_ref()),
"clone Git dependency",
)
}
Expand Down Expand Up @@ -537,10 +537,7 @@ fn require_local_repository(

fn verify_origin(destination: &Path, expected: &str) -> Result<(), String> {
let actual = git_stdout(
Command::new("git")
.arg("-C")
.arg(destination)
.args(["remote", "get-url", "origin"]),
git_command_in(destination).args(["remote", "get-url", "origin"]),
"read Git dependency origin",
)?;
if actual == expected {
Expand All @@ -554,18 +551,13 @@ fn verify_origin(destination: &Path, expected: &str) -> Result<(), String> {

fn git_fetch(destination: &Path) -> Result<(), String> {
run_git(
Command::new("git")
.arg("-C")
.arg(destination)
.args(["fetch", "origin", "--tags", "--prune"]),
git_command_in(destination).args(["fetch", "origin", "--tags", "--prune"]),
"fetch Git dependency",
)
}

fn git_has_commit(destination: &Path, commit: &str) -> Result<bool, String> {
let output = Command::new("git")
.arg("-C")
.arg(destination)
let output = git_command_in(destination)
.args(["cat-file", "-e", &format!("{commit}^{{commit}}")])
.output()
.map_err(|e| format!("failed to inspect Git dependency commit: {e}"))?;
Expand All @@ -581,10 +573,7 @@ fn require_checkout_at(destination: &Path, url: &str, commit: &str) -> Result<()
}
verify_origin(destination, url)?;
let current = git_stdout(
Command::new("git")
.arg("-C")
.arg(destination)
.args(["rev-parse", "HEAD"]),
git_command_in(destination).args(["rev-parse", "HEAD"]),
"read Git dependency HEAD",
)?;
if current != commit {
Expand All @@ -598,21 +587,15 @@ fn require_checkout_at(destination: &Path, url: &str, commit: &str) -> Result<()

fn checkout_commit(destination: &Path, commit: &str) -> Result<(), String> {
let current = git_stdout(
Command::new("git")
.arg("-C")
.arg(destination)
.args(["rev-parse", "HEAD"]),
git_command_in(destination).args(["rev-parse", "HEAD"]),
"read Git dependency HEAD",
)?;
if current == commit {
return Ok(());
}

let dirty = git_stdout(
Command::new("git")
.arg("-C")
.arg(destination)
.args(["status", "--porcelain"]),
git_command_in(destination).args(["status", "--porcelain"]),
"inspect Git dependency checkout",
)?;
if !dirty.is_empty() {
Expand All @@ -623,14 +606,51 @@ fn checkout_commit(destination: &Path, commit: &str) -> Result<(), String> {
}

run_git(
Command::new("git")
.arg("-C")
.arg(destination)
.args(["checkout", "--detach", commit]),
git_command_in(destination).args(["checkout", "--detach", commit]),
"checkout locked Git dependency commit",
)
}

fn git_command_in(destination: &Path) -> Command {
let mut command = Command::new("git");
command.arg("-C").arg(git_cli_path(destination).as_ref());
command
}

fn git_cli_path(path: &Path) -> Cow<'_, Path> {
#[cfg(windows)]
{
use std::path::{Component, Prefix};

let mut components = path.components();
let prefix = match components.next() {
Some(Component::Prefix(prefix)) => prefix,
_ => return Cow::Borrowed(path),
};
let mut normalized = match prefix.kind() {
Prefix::VerbatimDisk(drive) => PathBuf::from(format!("{}:\\", drive as char)),
Prefix::VerbatimUNC(server, share) => {
let mut normalized = PathBuf::from(r"\\");
normalized.push(server);
normalized.push(share);
normalized
}
_ => return Cow::Borrowed(path),
};
for component in components {
if !matches!(component, Component::RootDir) {
normalized.push(component.as_os_str());
}
}
Cow::Owned(normalized)
}

#[cfg(not(windows))]
{
Cow::Borrowed(path)
}
}

fn run_git(command: &mut Command, action: &str) -> Result<(), String> {
let output = command
.output()
Expand Down Expand Up @@ -694,4 +714,24 @@ mod tests {
};
assert_eq!(first, second);
}

#[cfg(windows)]
#[test]
fn git_cli_path_removes_verbatim_disk_prefix() {
let path = Path::new(r"\\?\C:\workspace\project\.vex\deps\package");
assert_eq!(
git_cli_path(path).as_ref(),
Path::new(r"C:\workspace\project\.vex\deps\package")
);
}

#[cfg(windows)]
#[test]
fn git_cli_path_removes_verbatim_unc_prefix() {
let path = Path::new(r"\\?\UNC\server\share\project\.vex\deps\package");
assert_eq!(
git_cli_path(path).as_ref(),
Path::new(r"\\server\share\project\.vex\deps\package")
);
}
}
13 changes: 7 additions & 6 deletions tests/git_lock_reproducibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};

mod support;
use support::git_url;

static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);

struct TestDir(PathBuf);

impl TestDir {
fn new() -> Self {
let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
let path =
std::env::temp_dir().join(format!("vex-git-lock-test-{}-{id}", std::process::id()));
let path = std::env::temp_dir().join(format!(
"vex git lock test-{}-{id}#fixture",
std::process::id()
));
fs::create_dir_all(&path).expect("test directory must be created");
Self(path)
}
Expand Down Expand Up @@ -195,10 +200,6 @@ fn git_stdout(path: &Path, args: &[&str]) -> String {
.to_string()
}

fn git_url(path: &Path) -> String {
format!("file://{}", path.to_string_lossy())
}

fn vex(path: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_vex"))
.args(args)
Expand Down
9 changes: 4 additions & 5 deletions tests/git_targeted_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};

mod support;
use support::git_url;

static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);

struct TestDir(PathBuf);
Expand All @@ -11,7 +14,7 @@ impl TestDir {
fn new() -> Self {
let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"vex-targeted-update-test-{}-{id}",
"vex targeted update test-{}-{id}#fixture",
std::process::id()
));
fs::create_dir_all(&path).expect("test directory must be created");
Expand Down Expand Up @@ -238,10 +241,6 @@ fn git_stdout(path: &Path, args: &[&str]) -> String {
.to_string()
}

fn git_url(path: &Path) -> String {
format!("file://{}", path.to_string_lossy())
}

fn vex(path: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_vex"))
.args(args)
Expand Down
Loading