From 07f74a891bb94dee363d6ad6b2b0e97e5ca6e965 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Fri, 16 Aug 2024 09:21:07 +0100 Subject: [PATCH 1/2] Add new CI test for NPM/PNPM installs in Windows --- .circleci/config.yml | 64 +++++++++++++++---- .circleci/scripts/{ => nix}/install_npm.sh | 2 +- .../scripts/{ => nix}/install_npm_global.sh | 2 +- .circleci/scripts/{ => nix}/install_pnpm.sh | 4 +- .circleci/scripts/windows/install_npm.ps1 | 41 ++++++++++++ .../scripts/windows/install_npm_global.ps1 | 32 ++++++++++ .circleci/scripts/windows/install_pnpm.ps1 | 41 ++++++++++++ 7 files changed, 169 insertions(+), 17 deletions(-) rename .circleci/scripts/{ => nix}/install_npm.sh (93%) rename .circleci/scripts/{ => nix}/install_npm_global.sh (93%) rename .circleci/scripts/{ => nix}/install_pnpm.sh (88%) create mode 100644 .circleci/scripts/windows/install_npm.ps1 create mode 100644 .circleci/scripts/windows/install_npm_global.ps1 create mode 100644 .circleci/scripts/windows/install_pnpm.ps1 diff --git a/.circleci/config.yml b/.circleci/config.yml index 2d9566040..3680fc463 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -104,11 +104,17 @@ executors: environment: XTASK_TARGET: "x86_64-unknown-linux-gnu" - node_js: + node_js_nix: docker: - - image: node:lts + - image: node:20.16.0 resource_class: medium + node_js_windows: + machine: + image: "windows-server-2019-vs2019:2022.08.1" + shell: powershell.exe -ExecutionPolicy Bypass + resource_class: windows.medium + tag_matches_prerelease: &tag_matches_prerelease matches: @@ -171,10 +177,11 @@ workflows: rust_channel: [stable] command: [integration-test] - install_js: - name: Test installation for Javascript Package Managers (<< matrix.package_manager >>) + name: Test installation for Javascript Package Managers (<< matrix.package_manager >> on << matrix.platform >>) matrix: parameters: package_manager: [npm, npm_global, pnpm] + platform: [windows, nix] - node/test: name: Test NPM Installer Scripts app-dir: "~/project/installers/npm" @@ -216,10 +223,11 @@ workflows: <<: *run_release - install_js: - name: Test installation for Javascript Package Managers (<< matrix.package_manager >>) + name: Test installation for Javascript Package Managers (<< matrix.package_manager >> on << matrix.platform >>) matrix: parameters: package_manager: [ npm, npm_global, pnpm ] + platform: [windows, nix] <<: *run_release - node/test: @@ -244,9 +252,12 @@ workflows: - "Run cargo tests + studio integration tests (stable rust on amd_windows)" - "Run studio integration tests in GitHub Actions (amd_macos)" - "Run supergraph-demo tests (stable rust on amd_ubuntu)" - - "Test installation for Javascript Package Managers (npm)" - - "Test installation for Javascript Package Managers (npm_global)" - - "Test installation for Javascript Package Managers (pnpm)" + - "Test installation for Javascript Package Managers (npm on nix)" + - "Test installation for Javascript Package Managers (npm_global on nix)" + - "Test installation for Javascript Package Managers (pnpm on nix)" + - "Test installation for Javascript Package Managers (npm on windows)" + - "Test installation for Javascript Package Managers (npm_global on windows)" + - "Test installation for Javascript Package Managers (pnpm on windows)" - "Test NPM Installer Scripts" <<: *run_release @@ -348,15 +359,42 @@ jobs: package_manager: type: enum enum: ["npm", "npm_global", "pnpm"] - executor: node_js + platform: + type: enum + enum: ["nix", "windows"] + executor: node_js_<> steps: - checkout: path: "rover" - - run: - name: "Invoke Install Scripts" - command: | - cd rover/.circleci/scripts - ./install_<< parameters.package_manager >>.sh + - when: + condition: + equal: ["nix", <>] + steps: + - run: + name: "Invoke Install Scripts (Unix)" + command: | + cd rover/.circleci/scripts/<> + ./install_<< parameters.package_manager >>.sh + - when: + condition: + equal: ["windows", <>] + steps: + - run: + name: "Invoke Install Scripts (Windows)" + command: | + Write-Output "Installing Volta" + choco install volta + refreshenv + Write-Output "Installing Node & NPM" + volta install node@20.16.0 + Write-Output "Checking Node & NPM version" + node --version + npm --version + + $script_location=Join-Path rover\.circleci\scripts << parameters.platform >> + Set-Location $script_location + .\install_<< parameters.package_manager >>.ps1 + # reusable command snippets can be referred to in any `steps` object commands: diff --git a/.circleci/scripts/install_npm.sh b/.circleci/scripts/nix/install_npm.sh similarity index 93% rename from .circleci/scripts/install_npm.sh rename to .circleci/scripts/nix/install_npm.sh index 5c7c3b669..52f3f19c8 100755 --- a/.circleci/scripts/install_npm.sh +++ b/.circleci/scripts/nix/install_npm.sh @@ -2,7 +2,7 @@ set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -INSTALLERS_DIR="$SCRIPT_DIR/../../installers/npm" +INSTALLERS_DIR="$SCRIPT_DIR/../../../installers/npm" cd "$(mktemp -d)" echo "Created test directory" diff --git a/.circleci/scripts/install_npm_global.sh b/.circleci/scripts/nix/install_npm_global.sh similarity index 93% rename from .circleci/scripts/install_npm_global.sh rename to .circleci/scripts/nix/install_npm_global.sh index 907c3eaff..aa8122ce1 100755 --- a/.circleci/scripts/install_npm_global.sh +++ b/.circleci/scripts/nix/install_npm_global.sh @@ -2,7 +2,7 @@ set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -INSTALLERS_DIR="$SCRIPT_DIR/../../installers/npm" +INSTALLERS_DIR="$SCRIPT_DIR/../../../installers/npm" cd "$(mktemp -d)" echo "Created test directory" diff --git a/.circleci/scripts/install_pnpm.sh b/.circleci/scripts/nix/install_pnpm.sh similarity index 88% rename from .circleci/scripts/install_pnpm.sh rename to .circleci/scripts/nix/install_pnpm.sh index 49aee8e20..cb6f0e06e 100755 --- a/.circleci/scripts/install_pnpm.sh +++ b/.circleci/scripts/nix/install_pnpm.sh @@ -2,7 +2,7 @@ set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -INSTALLERS_DIR="$SCRIPT_DIR/../../installers/npm" +INSTALLERS_DIR="$SCRIPT_DIR/../../../installers/npm" cd "$(mktemp -d)" echo "Created test directory" @@ -14,7 +14,7 @@ echo "Installed pnpm" npm --prefix "$INSTALLERS_DIR" version --allow-same-version 0.23.0 echo "Temporarily patched package.json to fixed stable binary" pnpm init -pnpm add "file:$SCRIPT_DIR/../../installers/npm" +pnpm add "file:$INSTALLERS_DIR" echo "Installed rover as pnpm package" cd node_modules/.bin/ echo "Checking version" diff --git a/.circleci/scripts/windows/install_npm.ps1 b/.circleci/scripts/windows/install_npm.ps1 new file mode 100644 index 000000000..e0c60a96a --- /dev/null +++ b/.circleci/scripts/windows/install_npm.ps1 @@ -0,0 +1,41 @@ +$ErrorActionPreference = "Stop" + +Function New-TemporaryFolder { + # Make a new folder based upon a TempFileName + $TEMP_PATH=[System.IO.Path]::GetTempPath() + $T= Join-Path $TEMP_PATH tmp$([convert]::tostring((get-random 65535),16).padleft(4,'0')) + New-Item -ItemType Directory -Path $T +} + +# Find the installers directory +$installer_dir = [System.IO.Path]::Combine($PSScriptRoot, "..", "..", "..", "installers", "npm") +Write-Output "Found installers directory at $installer_dir" + +# Create a temporary folder for the test +$test_dir = New-TemporaryFolder +Write-Output "Created test directory at $test_dir" +Set-Location $test_dir +# Initialise an empty NPM package +npm init -y +Write-Output "Initialised new npm package" + +# The choice of version here is arbitrary (we just need something we know exists) so that we can test if the +# installer works, given an existing version. This way we're not at the mercy of whether the binary that corresponds +# to the latest commit exists. +npm --prefix "$installer_dir" version --allow-same-version 0.23.0 +Write-Output "Temporarily patched package.json to fixed stable binary" + +# Install all the dependencies, including `rover` +npm install --install-links=true "$installer_dir" +Write-Output "Installed rover as local npm package" + +# Move to the installed location +$node_modules_path=[System.IO.Path]::Combine($test_dir, "node_modules", ".bin") +Set-Location $node_modules_path + +# Check the version +Write-Output "Checking version" +$dir_sep=[IO.Path]::DirectorySeparatorChar +$rover_command=".${dir_sep}rover --version" +Invoke-Expression $rover_command +Write-Output "Checked version, all ok!" diff --git a/.circleci/scripts/windows/install_npm_global.ps1 b/.circleci/scripts/windows/install_npm_global.ps1 new file mode 100644 index 000000000..f80b81d9e --- /dev/null +++ b/.circleci/scripts/windows/install_npm_global.ps1 @@ -0,0 +1,32 @@ +$ErrorActionPreference = "Stop" + +Function New-TemporaryFolder { + # Make a new folder based upon a TempFileName + $TEMP_PATH=[System.IO.Path]::GetTempPath() + $T= Join-Path $TEMP_PATH tmp$([convert]::tostring((get-random 65535),16).padleft(4,'0')) + New-Item -ItemType Directory -Path $T +} + +# Find the installers directory +$installer_dir = [System.IO.Path]::Combine($PSScriptRoot, "..", "..", "..", "installers", "npm") +Write-Output "Found installers directory at $installer_dir" + +# Create a temporary folder for the test +$test_dir = New-TemporaryFolder +Write-Output "Created test directory at $test_dir" +Set-Location $test_dir + +# The choice of version here is arbitrary (we just need something we know exists) so that we can test if the +# installer works, given an existing version. This way we're not at the mercy of whether the binary that corresponds +# to the latest commit exists. +npm --prefix "$installer_dir" version --allow-same-version 0.23.0 +Write-Output "Temporarily patched package.json to fixed stable binary" + +# Install all the dependencies, including `rover` +npm install --install-links=true -g "$installer_dir" +Write-Output "Installed rover as global npm package" + +# Check the version +Write-Output "Checking version" +rover --version +Write-Output "Checked version, all ok!" diff --git a/.circleci/scripts/windows/install_pnpm.ps1 b/.circleci/scripts/windows/install_pnpm.ps1 new file mode 100644 index 000000000..6f9f98415 --- /dev/null +++ b/.circleci/scripts/windows/install_pnpm.ps1 @@ -0,0 +1,41 @@ +$ErrorActionPreference = "Stop" + +Function New-TemporaryFolder { + # Make a new folder based upon a TempFileName + $TEMP_PATH=[System.IO.Path]::GetTempPath() + $T= Join-Path $TEMP_PATH tmp$([convert]::tostring((get-random 65535),16).padleft(4,'0')) + New-Item -ItemType Directory -Path $T +} + +# Find the installers directory +$installer_dir = [System.IO.Path]::Combine($PSScriptRoot, "..", "..", "..", "installers", "npm") +Write-Output "Found installers directory at $installer_dir" + +# Create a temporary folder for the test +$test_dir = New-TemporaryFolder +Write-Output "Created test directory at $test_dir" +Set-Location $test_dir +# Install pnpm +npm install -g pnpm@v9.3.0 + +# The choice of version here is arbitrary (we just need something we know exists) so that we can test if the +# installer works, given an existing version. This way we're not at the mercy of whether the binary that corresponds +# to the latest commit exists. +npm --prefix "$installer_dir" version --allow-same-version 0.23.0 +Write-Output "Temporarily patched package.json to fixed stable binary" + +# Install all the dependencies, including `rover` +pnpm init +pnpm add "file:$installer_dir" +Write-Output "Installed rover as local npm package" + +# Move to the installed location +$node_modules_path=[System.IO.Path]::Combine($test_dir, "node_modules", ".bin") +Set-Location $node_modules_path + +# Check the version +Write-Output "Checking version" +$dir_sep=[IO.Path]::DirectorySeparatorChar +$rover_command=".${dir_sep}rover --version" +Invoke-Expression $rover_command +Write-Output "Checked version, all ok!" From d2f443b56044ea9418d52e9d95ffded9fe4c69f7 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Fri, 16 Aug 2024 11:00:10 +0100 Subject: [PATCH 2/2] Fix the incorrect Windows behaviour --- installers/npm/binary.js | 20 +++++++------ installers/npm/tests/binary.test.js | 27 +++++++++++++++++- .../fake_tarballs/rover-fake-windows.tar.gz | Bin 0 -> 5240 bytes 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 installers/npm/tests/fake_tarballs/rover-fake-windows.tar.gz diff --git a/installers/npm/binary.js b/installers/npm/binary.js index d8198efbb..230955e19 100644 --- a/installers/npm/binary.js +++ b/installers/npm/binary.js @@ -25,37 +25,39 @@ const supportedPlatforms = [ ARCHITECTURE: "x64", RUST_TARGET: "x86_64-pc-windows-msvc", BINARY_NAME: `${name}-${version}.exe`, + RAW_NAME: `${name}.exe` }, { TYPE: "Linux", ARCHITECTURE: "x64", RUST_TARGET: "x86_64-unknown-linux-gnu", BINARY_NAME: `${name}-${version}`, + RAW_NAME: `${name}` }, { TYPE: "Linux", ARCHITECTURE: "arm64", RUST_TARGET: "aarch64-unknown-linux-gnu", BINARY_NAME: `${name}-${version}`, + RAW_NAME: `${name}` }, { TYPE: "Darwin", ARCHITECTURE: "x64", RUST_TARGET: "x86_64-apple-darwin", BINARY_NAME: `${name}-${version}`, + RAW_NAME: `${name}` }, { TYPE: "Darwin", ARCHITECTURE: "arm64", RUST_TARGET: "aarch64-apple-darwin", BINARY_NAME: `${name}-${version}`, + RAW_NAME: `${name}` }, ]; -const getPlatform = () => { - const type = os.type(); - const architecture = os.arch(); - +const getPlatform = (type = os.type(), architecture = os.arch()) => { for (let supportedPlatform of supportedPlatforms) { if ( type === supportedPlatform.TYPE && @@ -102,7 +104,7 @@ const getPlatform = () => { /*! Copyright (c) 2019 Avery Harnish - MIT License */ class Binary { - constructor(name, url, installDirectory) { + constructor(name, raw_name, url, installDirectory) { let errors = []; if (typeof url !== "string") { errors.push("url must be a string"); @@ -132,6 +134,7 @@ class Binary { } this.url = url; this.name = name; + this.raw_name = raw_name; this.installDirectory = installDirectory; if (!existsSync(this.installDirectory)) { @@ -176,7 +179,7 @@ class Binary { }); }) .then(() => { - fs.renameSync(join(this.installDirectory, name), this.binaryPath); + fs.renameSync(join(this.installDirectory, this.raw_name), this.binaryPath); if (!suppressLogs) { console.error(`${this.name} has been installed!`); } @@ -212,8 +215,7 @@ class Binary { } } -const getBinary = (overrideInstallDirectory) => { - const platform = getPlatform(); +const getBinary = (overrideInstallDirectory, platform = getPlatform()) => { const download_host = process.env.npm_config_apollo_rover_download_host || process.env.APOLLO_ROVER_DOWNLOAD_HOST || 'https://rover.apollo.dev' // the url for this binary is constructed from values in `package.json` // https://rover.apollo.dev/tar/rover/x86_64-unknown-linux-gnu/v0.4.8 @@ -224,7 +226,7 @@ const getBinary = (overrideInstallDirectory) => { if (overrideInstallDirectory != null && overrideInstallDirectory !== "") { installDirectory = overrideInstallDirectory } - let binary = new Binary(platform.BINARY_NAME, url, installDirectory); + let binary = new Binary(platform.BINARY_NAME, platform.RAW_NAME, url, installDirectory); // setting this allows us to extract supergraph plugins to the proper directory // the variable itself is read in Rust code diff --git a/installers/npm/tests/binary.test.js b/installers/npm/tests/binary.test.js index d48b0a025..00d8b617c 100644 --- a/installers/npm/tests/binary.test.js +++ b/installers/npm/tests/binary.test.js @@ -6,9 +6,18 @@ const fs = require("node:fs"); const crypto = require("node:crypto"); const MockAdapter = require("axios-mock-adapter"); const axios = require("axios"); +const {getPlatform} = require("../binary"); var mock = new MockAdapter(axios); -mock.onGet(new RegExp("https://rover\.apollo\.dev.*")).reply(function (_) { +mock.onGet(new RegExp("https://rover\.apollo\.dev/tar/rover/x86_64-pc-windows-msvc/.*")).reply(function (_) { + return [ + 200, + fs.createReadStream( + path.join(__dirname, "fake_tarballs", "rover-fake-windows.tar.gz"), + ), + ]; +}); +mock.onGet(new RegExp("https://rover\.apollo\.dev/tar/rover/.*")).reply(function (_) { return [ 200, fs.createReadStream( @@ -17,6 +26,7 @@ mock.onGet(new RegExp("https://rover\.apollo\.dev.*")).reply(function (_) { ]; }); + test("getBinary should be created with correct name and URL", () => { fs.mkdtempSync(path.join(os.tmpdir(), "rover-tests-")); const bin = binary.getBinary(os.tmpdir()); @@ -129,6 +139,21 @@ test("install renames binary properly", async () => { ).toHaveLength(1); }); +test("install renames binary properly (Windows)", async () => { + // Establish temporary directory + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "rover-tests-")); + // Create a Binary object + const bin = binary.getBinary(directory, getPlatform("Windows_NT", "x64")); + const directory_entries = await bin.install({}, true).then(async () => { + return fs.readdirSync(directory, { withFileTypes: true }); + }); + expect( + directory_entries.filter( + (d) => d.isFile() && d.name === `rover-${pjson.version}.exe`, + ), + ).toHaveLength(1); +}); + test("install adds a new binary if another version exists", async () => { // Create the temporary directory const directory = fs.mkdtempSync(path.join(os.tmpdir(), "rover-tests-")); diff --git a/installers/npm/tests/fake_tarballs/rover-fake-windows.tar.gz b/installers/npm/tests/fake_tarballs/rover-fake-windows.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..8546560310c599d6393747d8ee2e13df6117dfd4 GIT binary patch literal 5240 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 yoyuo-`15P;n-3o