diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000000..5c911c6c95 --- /dev/null +++ b/.github/workflows/release.yaml @@ -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* diff --git a/cli/src/config.rs b/cli/src/config.rs index c8a579f3a2..7342bb5591 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -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 ` 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. diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 52b22d724d..79e5c27e4d 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -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 { - 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") @@ -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); @@ -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"); } @@ -1107,22 +1119,13 @@ fn init( Ok(()) } -fn install_node_modules(cmd: &str) -> Result { - 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::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/` directory. @@ -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, @@ -1915,7 +1933,21 @@ fn _build_rust_cwd( arch: &ProgramArch, cargo_args: Vec, ) -> 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())