Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
252e2f4
Adds Windows support
arcanis Jul 16, 2026
68aa83a
Fixes Windows release archive layout
arcanis Jul 16, 2026
9c2c56d
Normalizes Windows verbatim paths
arcanis Jul 16, 2026
cf2b035
Uses portable paths internally on Windows
arcanis Jul 16, 2026
7d5090e
Kills Windows daemon process trees
arcanis Jul 16, 2026
98bc564
Supports CRLF JSON documents
arcanis Jul 16, 2026
036daa8
Raises the Windows CLI stack reserve
arcanis Jul 16, 2026
695fea8
Supports Windows Node builtin variants
arcanis Jul 16, 2026
1746cf0
Allows test setup to refresh lock metadata
arcanis Jul 16, 2026
6ac9b8a
Fixes Windows junction handling
arcanis Jul 16, 2026
e282b6b
Secures Windows daemon registry files
arcanis Jul 16, 2026
29858d1
Handles Windows ARM64 install scripts
arcanis Jul 16, 2026
9594f7f
Preserves extended UNC paths
arcanis Jul 16, 2026
745da1f
Handles broken junction targets
arcanis Jul 16, 2026
4c4f6a6
Fix Windows test reporting workflow
arcanis Jul 16, 2026
a39b539
Fix acceptance test reporting stability
arcanis Jul 16, 2026
3790ed8
Stabilize pushed subtask exit test
arcanis Jul 16, 2026
6d8b81c
Fix Windows native path boundaries
arcanis Jul 16, 2026
c7e6b0d
Test Windows native path commands
arcanis Jul 16, 2026
10fee14
Fix Windows native path boundaries
arcanis Jul 16, 2026
dd30447
Pass native paths to git on Windows
arcanis Jul 16, 2026
d07d5d5
Use absolute symlink targets across Windows drives
arcanis Jul 16, 2026
39c2179
Avoid junction churn for relative link targets
arcanis Jul 16, 2026
d1f0a02
Preserve linked portal path spelling
arcanis Jul 16, 2026
794e9a0
Resolve Windows command shims before spawning
arcanis Jul 16, 2026
7f9c5d5
Ignore extensionless shims on Windows spawn
arcanis Jul 16, 2026
d95fbd1
Pass native pack paths to external Yarn
arcanis Jul 16, 2026
215c469
Guard relative link paths across Windows drives
arcanis Jul 17, 2026
b9b94a6
Propagate pushed subtask exit codes
arcanis Jul 17, 2026
51c1b87
Normalize Windows git diff paths
arcanis Jul 17, 2026
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
4 changes: 2 additions & 2 deletions .github/actions/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ runs:
using: composite
steps:
- name: Install cross
if: ${{ !endsWith(inputs.target, '-apple-darwin') && inputs.target != 'wasm32-browserpod-linux-musl' }}
if: ${{ !endsWith(inputs.target, '-apple-darwin') && !contains(inputs.target, '-windows-') && inputs.target != 'wasm32-browserpod-linux-musl' }}
uses: taiki-e/install-action@a27ef18d36cfa66b0af3a360104621793b41c036 # v2.54.3
with:
tool: cross
Expand Down Expand Up @@ -90,7 +90,7 @@ runs:
cargo "+browserpod-${{inputs.browserpod-version}}" build --verbose --bin yarn-bin --profile=${{inputs.profile}} --target=${{inputs.target}}

cp target/${{inputs.target}}/${{inputs.profile}}/yarn-bin target/${{inputs.target}}/${{inputs.profile}}/yarn-bin.wasm
elif [[ "${{inputs.target}}" == *-apple-darwin ]]; then
elif [[ "${{inputs.target}}" == *-apple-darwin || "${{inputs.target}}" == *-windows-* ]]; then
cargo build --verbose --profile=${{inputs.profile}} --target=${{inputs.target}}
else
cross build --verbose --profile=${{inputs.profile}} --target=${{inputs.target}}
Expand Down
9 changes: 8 additions & 1 deletion .github/actions/prepare-node/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ runs:
chmod +x "${{inputs.switch}}"
install_dir="${RUNNER_TEMP}/yarn-switch-bin"
mkdir -p "${install_dir}"
cp -f "${{inputs.switch}}" "${install_dir}/yarn"
if [[ "${RUNNER_OS}" == "Windows" ]]; then
cp -f "${{inputs.switch}}" "${install_dir}/yarn.exe"
else
cp -f "${{inputs.switch}}" "${install_dir}/yarn"
fi
echo "${install_dir}" >> "${GITHUB_PATH}"

- name: Add link to the local path
Expand All @@ -41,4 +45,7 @@ runs:
- name: Install dependencies
shell: bash
run: |
if [[ -n "${{inputs.switch}}" ]]; then
export YARN_ENABLE_IMMUTABLE_INSTALLS=0
fi
yarn install
33 changes: 24 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@ jobs:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
ext: ''
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
ext: ''
- target: i686-unknown-linux-musl
os: ubuntu-latest
ext: ''
- target: aarch64-apple-darwin
os: macos-latest
ext: ''
- target: x86_64-pc-windows-msvc
os: windows-latest
ext: '.exe'
- target: wasm32-browserpod-linux-musl
os: ubuntu-latest
ext: ''

name: "Building ${{matrix.target}}"
runs-on: ${{matrix.os}}
Expand Down Expand Up @@ -66,8 +74,8 @@ jobs:
with:
name: yarn-${{matrix.target}}
path: |
target/${{matrix.target}}/release-lto-nodebug/yarn-bin
target/${{matrix.target}}/release-lto-nodebug/yarn
target/${{matrix.target}}/release-lto-nodebug/yarn-bin${{matrix.ext}}
target/${{matrix.target}}/release-lto-nodebug/yarn${{matrix.ext}}
target/${{matrix.target}}/release-lto-nodebug/LICENSE.md

- name: Upload Browserpod artifacts
Expand All @@ -92,6 +100,10 @@ jobs:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
ext: ''
- target: x86_64-pc-windows-msvc
os: windows-latest
ext: '.exe'

name: "Testing ${{matrix.target}}"
needs: [build]
Expand All @@ -112,17 +124,20 @@ jobs:
- name: Prepare Node
uses: ./.github/actions/prepare-node
with:
link: artifacts/yarn-bin
switch: artifacts/yarn
link: artifacts/yarn-bin${{matrix.ext}}
switch: artifacts/yarn${{matrix.ext}}

- name: Generate the test report
shell: bash
env:
TEST_BINARY: ${{github.workspace}}/artifacts/yarn-bin${{matrix.ext}}
TEST_SWITCH_BINARY: ${{github.workspace}}/artifacts/yarn${{matrix.ext}}
run: |
chmod +x artifacts/{yarn,yarn-bin}
if [[ "${RUNNER_OS}" != "Windows" ]]; then
chmod +x artifacts/{yarn,yarn-bin}
fi

export TEST_BINARY=$(pwd)/artifacts/yarn-bin
export TEST_SWITCH_BINARY=$(pwd)/artifacts/yarn

yarn ./tests/acceptance-tests jest --json --outputFile=$(pwd)/report.json || true
yarn ./tests/acceptance-tests jest --json --outputFile="${GITHUB_WORKSPACE}/report.json" || true

- name: Upload test report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
Expand Down
28 changes: 26 additions & 2 deletions .github/workflows/releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,19 @@ jobs:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
ext: ''
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
ext: ''
- target: i686-unknown-linux-musl
os: ubuntu-latest
ext: ''
- target: aarch64-apple-darwin
os: macos-latest
ext: ''
- target: x86_64-pc-windows-msvc
os: windows-latest
ext: '.exe'

name: 'Building ${{matrix.target}}'
runs-on: ${{matrix.os}}
Expand All @@ -70,8 +77,24 @@ jobs:
version: ${{needs.check.outputs.version}}

- name: Generate the archive files
if: runner.os != 'Windows'
shell: bash
run: |
zip -jX yarn-${{matrix.target}}.zip target/${{matrix.target}}/release-lto-nodebug/{yarn-bin,yarn,LICENSE.md}
zip -jX yarn-${{matrix.target}}.zip \
target/${{matrix.target}}/release-lto-nodebug/yarn-bin${{matrix.ext}} \
target/${{matrix.target}}/release-lto-nodebug/yarn${{matrix.ext}} \
target/${{matrix.target}}/release-lto-nodebug/LICENSE.md

- name: Generate the archive files
if: runner.os == 'Windows'
shell: pwsh
run: |
$archiveRoot = New-Item -ItemType Directory -Path "archive-${{matrix.target}}"
Copy-Item "target/${{matrix.target}}/release-lto-nodebug/yarn-bin${{matrix.ext}}" $archiveRoot
Copy-Item "target/${{matrix.target}}/release-lto-nodebug/yarn${{matrix.ext}}" $archiveRoot
Copy-Item "target/${{matrix.target}}/release-lto-nodebug/LICENSE.md" $archiveRoot
Compress-Archive -Path "$archiveRoot/*" -DestinationPath "yarn-${{matrix.target}}.zip"
Remove-Item -Recurse -Force $archiveRoot

- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
Expand Down Expand Up @@ -173,7 +196,7 @@ jobs:
const ext = includes(targetName, `windows`) ? `.exe` : ``;

// We don't need to distribute Yarn Switch itself
fs.unlinkSync(path.join(destinationPath, `yarn`));
fs.unlinkSync(path.join(destinationPath, `yarn${ext}`));

// Rename the binary to `yarn`
fs.renameSync(path.join(destinationPath, `yarn-bin${ext}`), path.join(destinationPath, `yarn${ext}`));
Expand All @@ -190,6 +213,7 @@ jobs:
const os = findFirst(targetName, {
linux: `linux`,
darwin: `darwin`,
win32: `windows`,
});

fs.writeFileSync(path.join(destinationPath, `package.json`), `${JSON.stringify({
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ http-body-util = "0.1"
hyper = { version = "1", features = ["server", "http1"] }
hyper-util = { version = "0.1", features = ["tokio"] }
itertools = "0.14.0"
junction = "1.2.0"
indexmap = {version = "2.11.0", features = ["serde"]}
interprocess = { version = "2.2.2", features = ["tokio"] }
mimalloc = "0.1.43"
Expand Down Expand Up @@ -116,6 +117,7 @@ ts-rs = "12.0.1"
serde_json = { version = "1.0.145", features = ["preserve_order"] }
serde = { version = "1.0.207", features = ["derive"] }
wax = { git = "https://github.com/arcanis/wax.git" }
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading"] }
winnow = "0.7"
url = "2.5.7"
urlencoding = "2.1.3"
Expand Down
21 changes: 18 additions & 3 deletions install-script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ case $platform in
'Darwin arm64')
target=aarch64-apple-darwin
;;
'MINGW64_NT'*' x86_64' | 'MSYS_NT'*' x86_64' | 'CYGWIN_NT'*' x86_64')
target=x86_64-pc-windows-msvc
;;
Comment thread
cursor[bot] marked this conversation as resolved.
'MINGW64_NT'*' ARM64' | 'MINGW64_NT'*' aarch64' | 'MSYS_NT'*' ARM64' | 'MSYS_NT'*' aarch64' | 'CYGWIN_NT'*' ARM64' | 'CYGWIN_NT'*' aarch64')
target=x86_64-pc-windows-msvc
;;
'Linux aarch64' | 'Linux arm64')
target=aarch64-unknown-linux-musl
;;
Expand All @@ -82,6 +88,15 @@ case $platform in
;;
esac

case $target in
*-windows-*)
ext=.exe
;;
*)
ext=
;;
esac

install_dir=$HOME/.yarn/switch/bin
tmp_dir=$install_dir.tmp
archive=$tmp_dir/yarn.zip
Expand All @@ -100,16 +115,16 @@ curl --fail --location --progress-bar --output "$archive" $yarn_uri ||
error "Failed to download Yarn from $(colorize "$color_url" "$yarn_uri")"

unzip -q "$archive" -d "$tmp_dir"
rm "$tmp_dir"/yarn-bin
rm "$tmp_dir"/yarn-bin$ext
rm "$archive"

if [[ -n "$bin_dir" ]]; then
mv -f "$tmp_dir"/yarn "$bin_dir"/
mv -f "$tmp_dir"/yarn$ext "$bin_dir"/
else
rm -rf "$install_dir"
mv "$tmp_dir" "$install_dir"

echo

"$install_dir"/yarn switch postinstall -H "$HOME"
"$install_dir"/yarn$ext switch postinstall -H "$HOME"
fi
10 changes: 3 additions & 7 deletions packages/zpm-formats/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{borrow::Cow, os::unix::fs::PermissionsExt};
use std::borrow::Cow;

use libdeflater::{CompressionLvl, Compressor};
use zpm_utils::{FromFileString, impl_file_string_from_str, Path, ToFileString, ToHumanString};
Expand Down Expand Up @@ -190,9 +190,7 @@ pub fn entries_from_folder<'a>(path: &Path) -> Result<Vec<Entry<'a>>, Error> {

let rel_path = entry_path.relative_to(&base);
let data = entry_path.fs_read()?;
let metadata = entry_path.fs_metadata()?;

let is_exec = metadata.permissions().mode() & 0o111 != 0;
let is_exec = entry_path.fs_is_executable()?;
let mode = if is_exec { 0o755 } else { 0o644 };

entries.push(Entry {
Expand All @@ -216,9 +214,7 @@ pub fn entries_from_files<'a>(base: &Path, files: &[Path]) -> Result<Vec<Entry<'
.with_join(rel_path);

let data = abs_path.fs_read()?;
let metadata = abs_path.fs_metadata()?;

let is_exec = metadata.permissions().mode() & 0o111 != 0;
let is_exec = abs_path.fs_is_executable()?;
let mode = if is_exec { 0o755 } else { 0o644 };

entries.push(Entry {
Expand Down
6 changes: 4 additions & 2 deletions packages/zpm-parsers/src/json_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,13 +619,13 @@ impl<'a> Scanner<'a> {
}

fn skip_whitespace(&mut self) {
while self.offset < self.input.len() && (self.input[self.offset] == b' ' || self.input[self.offset] == b'\t' || self.input[self.offset] == b'\n') {
while self.offset < self.input.len() && matches!(self.input[self.offset], b' ' | b'\t' | b'\n' | b'\r') {
self.offset += 1;
}
}

fn rskip_whitespace(&mut self) {
while self.offset > 0 && (self.input[self.offset - 1] == b' ' || self.input[self.offset - 1] == b'\t' || self.input[self.offset - 1] == b'\n') {
while self.offset > 0 && matches!(self.input[self.offset - 1], b' ' | b'\t' | b'\n' | b'\r') {
self.offset -= 1;
}
}
Expand Down Expand Up @@ -935,6 +935,8 @@ mod tests {
// Edge cases with whitespace
#[case(b"{ \"spaced\": \"value\" }", vec!["spaced"], Value::String("updated".to_string()), b"{ \"spaced\": \"updated\" }")]
#[case(b"{\n\n \"key\": \"value\"\n\n}", vec!["key"], Value::String("new_value".to_string()), b"{\n\n \"key\": \"new_value\"\n\n}")]
#[case(b"{\r\n \"key\": \"value\"\r\n}", vec!["key"], Value::String("new_value".to_string()), b"{\r\n \"key\": \"new_value\"\r\n}")]
#[case(b"{\r\n \"key\": \"value\"\r\n}", vec!["another"], Value::String("new_value".to_string()), b"{\r\n \"another\": \"new_value\",\r\n \"key\": \"value\"\r\n}")]
#[case(b"{\"key\":\"no_spaces\"}", vec!["key"], Value::String("with_spaces".to_string()), b"{\"key\":\"with_spaces\"}")]

// Escaped characters
Expand Down
6 changes: 3 additions & 3 deletions packages/zpm-switch/src/commands/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ async fn proxy_completer_async(ctx: &CompletionContext<'_>) -> Vec<Completion> {
cmd
},
PackageManagerReference::Local(params) => {
std::process::Command::new(params.path.to_file_string())
std::process::Command::new(params.path.to_path_buf())
},
};

Expand All @@ -101,7 +101,7 @@ async fn proxy_completer_async(ctx: &CompletionContext<'_>) -> Vec<Completion> {
args.extend(user_args.into_iter().map(String::from));

if let Some(cwd) = get_fake_cwd() {
binary.args(&[cwd.to_file_string()]);
binary.args(&[cwd.to_native_string()]);
}

binary.args(&args);
Expand Down Expand Up @@ -161,7 +161,7 @@ impl ProxyCommand {
= self.args.clone();

if let Some(cwd) = get_fake_cwd() {
args.insert(0, cwd.to_file_string());
args.insert(0, cwd.to_native_string());
}

ExplicitCommand::run(&reference, &args).await
Expand Down
9 changes: 6 additions & 3 deletions packages/zpm-switch/src/commands/switch/daemon_open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ impl DaemonOpenCommand {
.arg("daemon")
.arg("--auth-token")
.arg(&auth_token)
.current_dir(detected_root.to_file_string())
.current_dir(detected_root.to_path_buf())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
Expand All @@ -135,8 +135,11 @@ impl DaemonOpenCommand {
binary.env("USERPROFILE", userprofile);
}

use std::os::unix::process::CommandExt;
binary.process_group(0);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
binary.process_group(0);
}

let mut child
= binary
Expand Down
2 changes: 1 addition & 1 deletion packages/zpm-switch/src/commands/switch/explicit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl ExplicitCommand {
=> install_package_manager(params).await?,

PackageManagerReference::Local(params)
=> Command::new(params.path.to_file_string()),
=> Command::new(params.path.to_path_buf()),
};

binary.stdout(Stdio::inherit());
Expand Down
Loading
Loading