From 538a2e88cd09e33c22823080e6a22fcabb31b306 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Tue, 11 Aug 2026 12:58:00 +0900 Subject: [PATCH 1/2] Fix Windows Git dependency paths Signed-off-by: LunaStev --- .github/workflows/ci.yml | 22 +------ CONTRIBUTING.md | 2 + README.md | 21 ++++++ src/resolver.rs | 106 ++++++++++++++++++++---------- tests/git_lock_reproducibility.rs | 13 ++-- tests/git_targeted_update.rs | 9 ++- tests/git_url.rs | 36 ++++++++++ tests/support/mod.rs | 49 ++++++++++++++ 8 files changed, 194 insertions(+), 64 deletions(-) create mode 100644 tests/git_url.rs create mode 100644 tests/support/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 404eb89..a83252a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,8 @@ 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 @@ -89,31 +91,11 @@ jobs: 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cab420f..868ae9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index 73320bc..24baa1f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/resolver.rs b/src/resolver.rs index d0d08ec..59cefc1 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; @@ -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)?; @@ -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", ) } @@ -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 { @@ -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 { - 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}"))?; @@ -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 { @@ -598,10 +587,7 @@ 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 { @@ -609,10 +595,7 @@ fn checkout_commit(destination: &Path, commit: &str) -> Result<(), String> { } 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() { @@ -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() @@ -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") + ); + } } diff --git a/tests/git_lock_reproducibility.rs b/tests/git_lock_reproducibility.rs index 142d3f7..278a9a5 100644 --- a/tests/git_lock_reproducibility.rs +++ b/tests/git_lock_reproducibility.rs @@ -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); @@ -10,8 +13,10 @@ 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) } @@ -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) diff --git a/tests/git_targeted_update.rs b/tests/git_targeted_update.rs index 0efdf24..953cb58 100644 --- a/tests/git_targeted_update.rs +++ b/tests/git_targeted_update.rs @@ -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); @@ -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"); @@ -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) diff --git a/tests/git_url.rs b/tests/git_url.rs new file mode 100644 index 0000000..c61b578 --- /dev/null +++ b/tests/git_url.rs @@ -0,0 +1,36 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 + +use std::path::Path; + +mod support; +use support::git_url; + +#[cfg(not(windows))] +#[test] +fn local_git_url_encodes_reserved_characters() { + assert_eq!( + git_url(Path::new("/tmp/vex fixture#1")), + "file:///tmp/vex%20fixture%231" + ); +} + +#[cfg(windows)] +#[test] +fn local_git_url_normalizes_verbatim_drive_paths() { + assert_eq!( + git_url(Path::new(r"\\?\C:\vex fixture#1")), + "file:///C:/vex%20fixture%231" + ); +} + +#[cfg(windows)] +#[test] +fn local_git_url_normalizes_verbatim_unc_paths() { + assert_eq!( + git_url(Path::new(r"\\?\UNC\server\share\vex fixture#1")), + "file://server/share/vex%20fixture%231" + ); +} diff --git a/tests/support/mod.rs b/tests/support/mod.rs new file mode 100644 index 0000000..aadf8b0 --- /dev/null +++ b/tests/support/mod.rs @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 + +use std::fmt::Write; +use std::path::Path; + +pub fn git_url(path: &Path) -> String { + let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + let path = path.to_string_lossy(); + + #[cfg(windows)] + let path = { + let path = path + .strip_prefix(r"\\?\UNC\") + .map(|rest| format!(r"\\{rest}")) + .unwrap_or_else(|| { + path.strip_prefix(r"\\?\") + .unwrap_or(path.as_ref()) + .to_string() + }); + path.replace('\\', "/") + }; + + #[cfg(not(windows))] + let path = path.into_owned(); + + let path = percent_encode_path(&path); + if path.starts_with("//") { + format!("file:{path}") + } else if path.starts_with('/') { + format!("file://{path}") + } else { + format!("file:///{path}") + } +} + +fn percent_encode_path(path: &str) -> String { + let mut encoded = String::with_capacity(path.len()); + for byte in path.bytes() { + if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'/' | b':') { + encoded.push(byte as char); + } else { + write!(encoded, "%{byte:02X}").expect("writing to a String cannot fail"); + } + } + encoded +} From febdf40691e312b00914e678505cdd123cb65530 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Tue, 11 Aug 2026 13:08:53 +0900 Subject: [PATCH 2/2] Update GitHub Actions runtimes Signed-off-by: LunaStev --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a83252a..1c091ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | @@ -34,7 +34,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: | @@ -42,7 +42,7 @@ jobs: rustup default stable - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.11" @@ -75,7 +75,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install Rust toolchain run: | @@ -83,7 +83,7 @@ jobs: rustup default stable - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.11" @@ -103,7 +103,7 @@ jobs: 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: |