Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Better Windows Support #3366

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
Draft
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
90 changes: 90 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Release

on:
workflow_dispatch:
push:
branches: [master, "feature/windows-support"]
tags:
- "v*.*.*"

jobs:
build:
strategy:
matrix:
os:
- windows-latest
- ubuntu-latest
- macos-latest
- macos-14

runs-on: ${{ matrix.os }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
override: true

- name: Set Version
id: set-version
shell: sh
run: |
VERSION=$(printf "%s" "${{ github.ref_name }}" | sed -e "s|/|-|g")
if [ "${{ github.ref_type }}" = "branch" ]; then
VERSION=$(printf "%s" "${GITHUB_SHA}" | cut -c-6)
fi
echo "version: ${VERSION}"
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"

- name: Build
id: build
shell: bash
run: |
# build avm and anchor separately for now because
# of the name conflict in avm/anchor and cli/anchor
cargo build --release -p anchor-cli --bin anchor
cargo build --release -p avm --bin avm

binary_extension=""

if [ "${RUNNER_OS}" = "Linux" ]; then
release_folder="ubuntu-latest"
elif [ "${RUNNER_OS}" = "Windows" ]; then
binary_extension=".exe"
release_folder="windows-latest"
elif [ "${RUNNER_OS}" = "macOS" ]; then
if [ "${{ matrix.os }}" = "macos-14" ]; then
release_folder="macos-m1-latest"
else
release_folder="macos-intel-latest"
fi
else
echo "unknown value for RUNNER_OS: ${RUNNER_OS}"
exit 1
fi

mkdir -p "${release_folder}"
echo "release-folder=${release_folder}" >> "${GITHUB_OUTPUT}"

mv "target/release/anchor${binary_extension}" "${release_folder}"
mv "target/release/avm${binary_extension}" "${release_folder}"
strip "${release_folder}"/*

- name: Publish Artifacts
if: "!startsWith(github.ref, 'refs/tags/')"
uses: actions/upload-artifact@v4
with:
name: "anchor-${{ steps.build.outputs.release-folder }}-${{ steps.set-version.outputs.version }}"
path: "${{ steps.build.outputs.release-folder }}/*"

- name: Release Tags
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
${{ steps.build.outputs.release-folder }}/avm*
${{ steps.build.outputs.release-folder }}/anchor*
13 changes: 13 additions & 0 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,19 @@ impl std::fmt::Display for PackageManager {
}
}

impl PackageManager {
/// On Windows, node executables use the `.cmd` suffix.
/// If not added, `process::Command::new()` will fail to find them.
/// `cmd /c <command>` is a workaround but it spawns an extra process.
/// This method returns the correct executable name for the current platform.
pub fn executable_name(&self) -> String {
#[cfg(windows)]
return format!("{self}.cmd");
#[cfg(not(windows))]
return self.to_string();
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FeaturesConfig {
/// Enable account resolution.
Expand Down
86 changes: 59 additions & 27 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ fn restore_toolchain(restore_cbs: RestoreToolchainCallbacks) -> Result<()> {

/// Get the system's default license - what 'npm init' would use.
fn get_npm_init_license() -> Result<String> {
let npm_init_license_output = std::process::Command::new("npm")
let npm_init_license_output = std::process::Command::new(PackageManager::NPM.executable_name())
.arg("config")
.arg("get")
.arg("init-license")
Expand Down Expand Up @@ -1000,8 +1000,7 @@ fn init(
let test_script = test_template.get_test_script(javascript, &package_manager);
cfg.scripts.insert("test".to_owned(), test_script);

let package_manager_cmd = package_manager.to_string();
cfg.toolchain.package_manager = Some(package_manager);
cfg.toolchain.package_manager = Some(package_manager.clone());

let mut localnet = BTreeMap::new();
let program_id = rust_template::get_or_create_program_id(&rust_name);
Expand Down Expand Up @@ -1077,14 +1076,27 @@ fn init(
)?;

if !no_install {
let package_manager_result = install_node_modules(&package_manager_cmd)?;

if !package_manager_result.status.success() && package_manager_cmd != "npm" {
let package_manager_result = install_node_modules(&package_manager);
// if the package manager is NOT `npm` and:
// 1. the install command failed to run (eg: binary not found), OR
// 2. the install command ran but didn't finish successfully
// then retry the install with npm
if package_manager != PackageManager::NPM
&& package_manager_result.map_or_else(
|err| {
// command failed to run
eprintln!("{}", err);
true
},
// command ran but didn't finish successfully
|output| !output.status.success(),
)
{
println!(
"Failed {} install will attempt to npm install",
package_manager_cmd
package_manager
);
install_node_modules("npm")?;
install_node_modules(&PackageManager::NPM)?;
} else {
eprintln!("Failed to install node modules");
}
Expand All @@ -1107,22 +1119,13 @@ fn init(
Ok(())
}

fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
if cfg!(target_os = "windows") {
std::process::Command::new("cmd")
.arg(format!("/C {cmd} install"))
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
} else {
std::process::Command::new(cmd)
.arg("install")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
}
fn install_node_modules(cmd: &PackageManager) -> Result<std::process::Output> {
std::process::Command::new(cmd.executable_name())
.arg("install")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
}

// Creates a new program crate in the `programs/<name>` directory.
Expand Down Expand Up @@ -1524,10 +1527,25 @@ fn build_rust_cwd(
no_docs: bool,
arch: &ProgramArch,
) -> Result<()> {
match cargo_toml.parent() {
#[cfg_attr(not(windows), allow(unused_mut))]
let mut cargo_toml_parent = match cargo_toml.parent() {
Some(parent) => parent.to_owned(),
None => return Err(anyhow!("Unable to find parent")),
Some(p) => std::env::set_current_dir(p)?,
};

// build-sbf depends on cargo metadata, which in turn depends on glob
// to find packages in a workspace
// but glob breaks on UNC paths like \\?\C:\...\packages\*
// so on windows, use the relative path of the package
// https://github.com/rust-lang/glob/issues/132
#[cfg(windows)]
{
let workspace_dir = cfg.path().parent().unwrap();
let relative_path = cargo_toml_parent.strip_prefix(workspace_dir.canonicalize()?)?;
cargo_toml_parent = std::path::absolute(workspace_dir.join(relative_path))?;
}

std::env::set_current_dir(cargo_toml_parent)?;
match build_config.verifiable {
false => _build_rust_cwd(
cfg, no_idl, idl_out, idl_ts_out, skip_lint, no_docs, arch, cargo_args,
Expand Down Expand Up @@ -1915,7 +1933,21 @@ fn _build_rust_cwd(
arch: &ProgramArch,
cargo_args: Vec<String>,
) -> Result<()> {
let exit = std::process::Command::new("cargo")
let mut build_command = std::process::Command::new("cargo");

// TODO: remove once all supported versions include the fix
// https://github.com/anza-xyz/agave/pull/3597
#[cfg(windows)]
if std::env::var("HOME").is_err() {
build_command.env(
"HOME",
std::env::var_os("USERPROFILE").ok_or_else(|| {
anyhow!("env variable 'HOME' not set and could not read 'USERPROFILE'")
})?,
);
}

let exit = build_command
.arg(arch.build_subcommand())
.args(cargo_args.clone())
.stdout(Stdio::inherit())
Expand Down