From 026d4a48209ffce47c41e48791c3d757b760bc33 Mon Sep 17 00:00:00 2001
From: Dmitry Shibanov <shibanov-1997@inbox.ru>
Date: Wed, 20 Dec 2023 16:52:29 +0100
Subject: [PATCH] add support for arm64 Windows

---
 __tests__/main.test.ts                 |  9 ++++++++
 __tests__/official-installer.test.ts   |  3 +++
 dist/cache-save/index.js               |  4 +++-
 dist/setup/index.js                    | 31 +++++++++++++++++++++-----
 src/distributions/base-distribution.ts | 25 ++++++++++++++++-----
 src/util.ts                            |  5 +++--
 6 files changed, 63 insertions(+), 14 deletions(-)

diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 36024e65..4ac99e0f 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -2,6 +2,7 @@ import * as core from '@actions/core';
 import * as exec from '@actions/exec';
 import * as tc from '@actions/tool-cache';
 import * as cache from '@actions/cache';
+import * as io from '@actions/io';
 
 import fs from 'fs';
 import path from 'path';
@@ -24,6 +25,8 @@ describe('main tests', () => {
   let startGroupSpy: jest.SpyInstance;
   let endGroupSpy: jest.SpyInstance;
 
+  let whichSpy: jest.SpyInstance;
+
   let existsSpy: jest.SpyInstance;
 
   let getExecOutputSpy: jest.SpyInstance;
@@ -56,6 +59,8 @@ describe('main tests', () => {
     inSpy = jest.spyOn(core, 'getInput');
     inSpy.mockImplementation(name => inputs[name]);
 
+    whichSpy = jest.spyOn(io, 'which');
+
     getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
 
     findSpy = jest.spyOn(tc, 'find');
@@ -126,6 +131,10 @@ describe('main tests', () => {
         return {stdout: obj[command], stderr: '', exitCode: 0};
       });
 
+      whichSpy.mockImplementation(cmd => {
+        return `some/${cmd}/path`;
+      });
+
       await util.printEnvDetailsAndSetOutput();
 
       expect(setOutputSpy).toHaveBeenCalledWith('node-version', obj['node']);
diff --git a/__tests__/official-installer.test.ts b/__tests__/official-installer.test.ts
index 2d36c19c..2d8f17cf 100644
--- a/__tests__/official-installer.test.ts
+++ b/__tests__/official-installer.test.ts
@@ -248,6 +248,9 @@ describe('setup-node', () => {
     const toolPath = path.normalize('/cache/node/12.16.2/x64');
     exSpy.mockImplementation(async () => '/some/other/temp/path');
     cacheSpy.mockImplementation(async () => toolPath);
+    whichSpy.mockImplementation(cmd => {
+      return `some/${cmd}/path`;
+    });
 
     await main.run();
 
diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js
index ece5ae39..3059e10e 100644
--- a/dist/cache-save/index.js
+++ b/dist/cache-save/index.js
@@ -83333,6 +83333,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
 exports.unique = exports.printEnvDetailsAndSetOutput = exports.parseNodeVersionFile = void 0;
 const core = __importStar(__nccwpck_require__(2186));
 const exec = __importStar(__nccwpck_require__(1514));
+const io = __importStar(__nccwpck_require__(7436));
 function parseNodeVersionFile(contents) {
     var _a, _b, _c;
     let nodeVersion;
@@ -83376,7 +83377,8 @@ function printEnvDetailsAndSetOutput() {
     return __awaiter(this, void 0, void 0, function* () {
         core.startGroup('Environment details');
         const promises = ['node', 'npm', 'yarn'].map((tool) => __awaiter(this, void 0, void 0, function* () {
-            const output = yield getToolVersion(tool, ['--version']);
+            const pathTool = yield io.which(tool, false);
+            const output = pathTool ? yield getToolVersion(tool, ['--version']) : '';
             return { tool, output };
         }));
         const tools = yield Promise.all(promises);
diff --git a/dist/setup/index.js b/dist/setup/index.js
index c4b448b1..c75d3211 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -93110,7 +93110,11 @@ class BaseDistribution {
         const fileName = this.osPlat == 'win32'
             ? `node-v${version}-win-${osArch}`
             : `node-v${version}-${this.osPlat}-${osArch}`;
-        const urlFileName = this.osPlat == 'win32' ? `${fileName}.7z` : `${fileName}.tar.gz`;
+        const urlFileName = this.osPlat == 'win32'
+            ? this.nodeInfo.arch === 'arm64'
+                ? `${fileName}.zip`
+                : `${fileName}.7z`
+            : `${fileName}.tar.gz`;
         const initialUrl = this.getDistributionUrl();
         const url = `${initialUrl}/v${version}/${urlFileName}`;
         return {
@@ -93194,10 +93198,18 @@ class BaseDistribution {
             let extPath;
             info = info || {}; // satisfy compiler, never null when reaches here
             if (this.osPlat == 'win32') {
-                const _7zPath = path.join(__dirname, '../..', 'externals', '7zr.exe');
-                extPath = yield tc.extract7z(downloadPath, undefined, _7zPath);
+                const extension = this.nodeInfo.arch === 'arm64' ? '.zip' : '.7z';
+                if (extension === '.zip') {
+                    const renamedArchive = `${downloadPath}.zip`;
+                    fs_1.default.renameSync(downloadPath, renamedArchive);
+                    extPath = yield tc.extractZip(renamedArchive);
+                }
+                else {
+                    const _7zPath = path.join(__dirname, '../..', 'externals', '7zr.exe');
+                    extPath = yield tc.extract7z(downloadPath, undefined, _7zPath);
+                }
                 // 7z extracts to folder matching file name
-                const nestedPath = path.join(extPath, path.basename(info.fileName, '.7z'));
+                const nestedPath = path.join(extPath, path.basename(info.fileName, extension));
                 if (fs_1.default.existsSync(nestedPath)) {
                     extPath = nestedPath;
                 }
@@ -93229,7 +93241,12 @@ class BaseDistribution {
                 dataFileName = `osx-${osArch}-tar`;
                 break;
             case 'win32':
-                dataFileName = `win-${osArch}-exe`;
+                if (this.nodeInfo.arch === 'arm64') {
+                    dataFileName = `win-${osArch}-zip`;
+                }
+                else {
+                    dataFileName = `win-${osArch}-7z`;
+                }
                 break;
             default:
                 throw new Error(`Unexpected OS '${this.osPlat}'`);
@@ -93784,6 +93801,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
 exports.unique = exports.printEnvDetailsAndSetOutput = exports.parseNodeVersionFile = void 0;
 const core = __importStar(__nccwpck_require__(2186));
 const exec = __importStar(__nccwpck_require__(1514));
+const io = __importStar(__nccwpck_require__(7436));
 function parseNodeVersionFile(contents) {
     var _a, _b, _c;
     let nodeVersion;
@@ -93827,7 +93845,8 @@ function printEnvDetailsAndSetOutput() {
     return __awaiter(this, void 0, void 0, function* () {
         core.startGroup('Environment details');
         const promises = ['node', 'npm', 'yarn'].map((tool) => __awaiter(this, void 0, void 0, function* () {
-            const output = yield getToolVersion(tool, ['--version']);
+            const pathTool = yield io.which(tool, false);
+            const output = pathTool ? yield getToolVersion(tool, ['--version']) : '';
             return { tool, output };
         }));
         const tools = yield Promise.all(promises);
diff --git a/src/distributions/base-distribution.ts b/src/distributions/base-distribution.ts
index edac6b9b..34fdcce6 100644
--- a/src/distributions/base-distribution.ts
+++ b/src/distributions/base-distribution.ts
@@ -112,7 +112,11 @@ export default abstract class BaseDistribution {
         ? `node-v${version}-win-${osArch}`
         : `node-v${version}-${this.osPlat}-${osArch}`;
     const urlFileName: string =
-      this.osPlat == 'win32' ? `${fileName}.7z` : `${fileName}.tar.gz`;
+      this.osPlat == 'win32'
+        ? this.nodeInfo.arch === 'arm64'
+          ? `${fileName}.zip`
+          : `${fileName}.7z`
+        : `${fileName}.tar.gz`;
     const initialUrl = this.getDistributionUrl();
     const url = `${initialUrl}/v${version}/${urlFileName}`;
 
@@ -215,12 +219,19 @@ export default abstract class BaseDistribution {
     let extPath: string;
     info = info || ({} as INodeVersionInfo); // satisfy compiler, never null when reaches here
     if (this.osPlat == 'win32') {
-      const _7zPath = path.join(__dirname, '../..', 'externals', '7zr.exe');
-      extPath = await tc.extract7z(downloadPath, undefined, _7zPath);
+      const extension = this.nodeInfo.arch === 'arm64' ? '.zip' : '.7z';
+      if (extension === '.zip') {
+        const renamedArchive = `${downloadPath}.zip`;
+        fs.renameSync(downloadPath, renamedArchive);
+        extPath = await tc.extractZip(renamedArchive);
+      } else {
+        const _7zPath = path.join(__dirname, '../..', 'externals', '7zr.exe');
+        extPath = await tc.extract7z(downloadPath, undefined, _7zPath);
+      }
       // 7z extracts to folder matching file name
       const nestedPath = path.join(
         extPath,
-        path.basename(info.fileName, '.7z')
+        path.basename(info.fileName, extension)
       );
       if (fs.existsSync(nestedPath)) {
         extPath = nestedPath;
@@ -260,7 +271,11 @@ export default abstract class BaseDistribution {
         dataFileName = `osx-${osArch}-tar`;
         break;
       case 'win32':
-        dataFileName = `win-${osArch}-exe`;
+        if (this.nodeInfo.arch === 'arm64') {
+          dataFileName = `win-${osArch}-zip`;
+        } else {
+          dataFileName = `win-${osArch}-7z`;
+        }
         break;
       default:
         throw new Error(`Unexpected OS '${this.osPlat}'`);
diff --git a/src/util.ts b/src/util.ts
index 0b2b1490..fd811ba3 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,5 +1,6 @@
 import * as core from '@actions/core';
 import * as exec from '@actions/exec';
+import * as io from '@actions/io';
 
 export function parseNodeVersionFile(contents: string): string | null {
   let nodeVersion: string | undefined;
@@ -44,9 +45,9 @@ export function parseNodeVersionFile(contents: string): string | null {
 
 export async function printEnvDetailsAndSetOutput() {
   core.startGroup('Environment details');
-
   const promises = ['node', 'npm', 'yarn'].map(async tool => {
-    const output = await getToolVersion(tool, ['--version']);
+    const pathTool = await io.which(tool, false);
+    const output = pathTool ? await getToolVersion(tool, ['--version']) : '';
 
     return {tool, output};
   });