diff --git a/.gitattributes b/.gitattributes index f6ac05f48..1f07b8568 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,5 @@ +# Set default behavior to automatically normalize line endings, and force everything to be LF, except for Windows batch files that require CRLF, so that if a repo is accessed in Unix via a file share from Windows, the scripts will work. +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf .licenses/** -diff linguist-generated=true diff --git a/.github/workflows/versions.yml b/.github/workflows/versions.yml index 3beb1f71d..4727b4c80 100644 --- a/.github/workflows/versions.yml +++ b/.github/workflows/versions.yml @@ -96,4 +96,17 @@ jobs: node-version: 0.12.18 - name: Verify node run: __tests__/verify-node.sh 0.12.18 SKIP_NPM + shell: bash + + arch: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup node 12 x86 from dist + uses: ./ + with: + node-version: '12' + architecture: 'x86' + - name: Verify node + run: __tests__/verify-arch.sh "ia32" shell: bash \ No newline at end of file diff --git a/README.md b/README.md index b271c0deb..911fbbec9 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,61 @@ jobs: - run: npm test ``` +Architecture: + +You can use any of the [supported operating systems](https://docs.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners), and the compatible `architecture` can be selected using `architecture`. Values are `x86`, `x64`, `arm64`, `armv6l`, `armv7l`, `ppc64le`, `s390x` (not all of the architectures are available on all platforms). + +When using `architecture`, `node-version` must be provided as well. +```yaml +jobs: + build: + runs-on: windows-latest + name: Node sample + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '12' + architecture: 'x64' # optional, x64 or x86. If not specified, x64 will be used by default + - run: npm install + - run: npm test +``` + +Multiple Operating Systems and Architectures: + +```yaml +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + node_version: + - 10 + - 12 + - 14 + architecture: + - x64 + # an extra windows-x86 run: + include: + - os: windows-2016 + node_version: 12 + architecture: x86 + name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Setup node + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node_version }} + architecture: ${{ matrix.architecture }} + - run: npm install + - run: npm test +``` + Publish to npmjs and GPR with npm: ```yaml steps: diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index dbbc4b446..22ac3d687 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -252,7 +252,7 @@ describe('setup-node', () => { expect(dlSpy).toHaveBeenCalled(); expect(exSpy).toHaveBeenCalled(); expect(logSpy).toHaveBeenCalledWith( - `Acquiring ${resolvedVersion} from ${expectedUrl}` + `Acquiring ${resolvedVersion} - ${os.arch} from ${expectedUrl}` ); expect(logSpy).toHaveBeenCalledWith( `Attempting to download ${versionSpec}...` @@ -341,6 +341,46 @@ describe('setup-node', () => { expect(cnSpy).toHaveBeenCalledWith(`::error::${errMsg}${osm.EOL}`); }); + it('Acquires specified architecture of node', async () => { + for (const {arch, version, osSpec} of [ + {arch: 'x86', version: '12.16.2', osSpec: 'win32'}, + {arch: 'x86', version: '14.0.0', osSpec: 'win32'} + ]) { + os.platform = osSpec; + os.arch = arch; + const fileExtension = os.platform === 'win32' ? '7z' : 'tar.gz'; + const platform = { + linux: 'linux', + darwin: 'darwin', + win32: 'win' + }[os.platform]; + + inputs['node-version'] = version; + inputs['architecture'] = arch; + inputs['always-auth'] = false; + inputs['token'] = 'faketoken'; + + let expectedUrl = + arch === 'x64' + ? `https://github.com/actions/node-versions/releases/download/${version}/node-${version}-${platform}-${arch}.zip` + : `https://nodejs.org/dist/v${version}/node-v${version}-${platform}-${arch}.${fileExtension}`; + + // ... but not in the local cache + findSpy.mockImplementation(() => ''); + + dlSpy.mockImplementation(async () => '/some/temp/path'); + let toolPath = path.normalize(`/cache/node/${version}/${arch}`); + exSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + expect(dlSpy).toHaveBeenCalled(); + expect(logSpy).toHaveBeenCalledWith( + `Acquiring ${version} - ${arch} from ${expectedUrl}` + ); + } + }, 100000); + describe('check-latest flag', () => { it('use local version and dont check manifest if check-latest is not specified', async () => { os.platform = 'linux'; @@ -403,7 +443,7 @@ describe('setup-node', () => { ); expect(logSpy).toHaveBeenCalledWith("Resolved as '12.16.2'"); expect(logSpy).toHaveBeenCalledWith( - `Acquiring 12.16.2 from ${expectedUrl}` + `Acquiring 12.16.2 - ${os.arch} from ${expectedUrl}` ); expect(logSpy).toHaveBeenCalledWith('Extracting ...'); }); diff --git a/__tests__/verify-arch.sh b/__tests__/verify-arch.sh new file mode 100644 index 000000000..7d4ebcbb8 --- /dev/null +++ b/__tests__/verify-arch.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if [ -n "$1" ]; then + architecture="$(node -e 'console.log(process.arch)')" + if [ -z "$(echo $architecture | grep --fixed-strings $1)" ]; then + echo "Unexpected architecture" + exit 1 + fi +else + echo "Skip testing architecture" +fi \ No newline at end of file diff --git a/action.yml b/action.yml index e5b901fcc..4b524c459 100644 --- a/action.yml +++ b/action.yml @@ -7,6 +7,8 @@ inputs: default: 'false' node-version: description: 'Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0' + architecture: + description: 'Target architecture for Node to use. Examples: x86, x64. Will use system architecture by default.' check-latest: description: 'Set this option if you want the action to check for the latest available version that satisfies the version spec' default: false diff --git a/dist/index.js b/dist/index.js index e0e73c554..c959e0a71 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4694,6 +4694,7 @@ const installer = __importStar(__webpack_require__(749)); const auth = __importStar(__webpack_require__(202)); const path = __importStar(__webpack_require__(622)); const url_1 = __webpack_require__(835); +const os = __webpack_require__(87); function run() { return __awaiter(this, void 0, void 0, function* () { try { @@ -4705,12 +4706,21 @@ function run() { if (!version) { version = core.getInput('version'); } + let arch = core.getInput('architecture'); + // if architecture supplied but node-version is not + // if we don't throw a warning, the already installed x64 node will be used which is not probably what user meant. + if (arch && !version) { + core.warning('`architecture` is provided but `node-version` is missing. In this configuration, the version/architecture of Node will not be changed. To fix this, provide `architecture` in combination with `node-version`'); + } + if (!arch) { + arch = os.arch(); + } if (version) { let token = core.getInput('token'); let auth = !token || isGhes() ? undefined : `token ${token}`; let stable = (core.getInput('stable') || 'true').toUpperCase() === 'TRUE'; const checkLatest = (core.getInput('check-latest') || 'false').toUpperCase() === 'TRUE'; - yield installer.getNode(version, stable, checkLatest, auth); + yield installer.getNode(version, stable, checkLatest, auth, arch); } const registryUrl = core.getInput('registry-url'); const alwaysAuth = core.getInput('always-auth'); @@ -13093,13 +13103,13 @@ const tc = __importStar(__webpack_require__(533)); const path = __importStar(__webpack_require__(622)); const semver = __importStar(__webpack_require__(280)); const fs = __webpack_require__(747); -function getNode(versionSpec, stable, checkLatest, auth) { +function getNode(versionSpec, stable, checkLatest, auth, arch = os.arch()) { return __awaiter(this, void 0, void 0, function* () { let osPlat = os.platform(); - let osArch = translateArchToDistUrl(os.arch()); + let osArch = translateArchToDistUrl(arch); if (checkLatest) { core.info('Attempt to resolve the latest version from manifest...'); - const resolvedVersion = yield resolveVersionFromManifest(versionSpec, stable, auth); + const resolvedVersion = yield resolveVersionFromManifest(versionSpec, stable, auth, osArch); if (resolvedVersion) { versionSpec = resolvedVersion; core.info(`Resolved as '${versionSpec}'`); @@ -13110,7 +13120,7 @@ function getNode(versionSpec, stable, checkLatest, auth) { } // check cache let toolPath; - toolPath = tc.find('node', versionSpec); + toolPath = tc.find('node', versionSpec, osArch); // If not found in cache, download if (toolPath) { core.info(`Found in cache @ ${toolPath}`); @@ -13123,9 +13133,9 @@ function getNode(versionSpec, stable, checkLatest, auth) { // Try download from internal distribution (popular versions only) // try { - info = yield getInfoFromManifest(versionSpec, stable, auth); + info = yield getInfoFromManifest(versionSpec, stable, auth, osArch); if (info) { - core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`); + core.info(`Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}`); downloadPath = yield tc.downloadTool(info.downloadUrl, undefined, auth); } else { @@ -13148,17 +13158,17 @@ function getNode(versionSpec, stable, checkLatest, auth) { // Download from nodejs.org // if (!downloadPath) { - info = yield getInfoFromDist(versionSpec); + info = yield getInfoFromDist(versionSpec, arch); if (!info) { throw new Error(`Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.`); } - core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`); + core.info(`Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}`); try { downloadPath = yield tc.downloadTool(info.downloadUrl); } catch (err) { if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { - return yield acquireNodeFromFallbackLocation(info.resolvedVersion); + return yield acquireNodeFromFallbackLocation(info.resolvedVersion, info.arch); } throw err; } @@ -13189,7 +13199,7 @@ function getNode(versionSpec, stable, checkLatest, auth) { // Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded // core.info('Adding to the cache ...'); - toolPath = yield tc.cacheDir(extPath, 'node', info.resolvedVersion); + toolPath = yield tc.cacheDir(extPath, 'node', info.resolvedVersion, info.arch); core.info('Done'); } // @@ -13206,26 +13216,27 @@ function getNode(versionSpec, stable, checkLatest, auth) { }); } exports.getNode = getNode; -function getInfoFromManifest(versionSpec, stable, auth) { +function getInfoFromManifest(versionSpec, stable, auth, osArch = translateArchToDistUrl(os.arch())) { return __awaiter(this, void 0, void 0, function* () { let info = null; const releases = yield tc.getManifestFromRepo('actions', 'node-versions', auth, 'main'); - const rel = yield tc.findFromManifest(versionSpec, stable, releases); + const rel = yield tc.findFromManifest(versionSpec, stable, releases, osArch); if (rel && rel.files.length > 0) { info = {}; info.resolvedVersion = rel.version; + info.arch = rel.files[0].arch; info.downloadUrl = rel.files[0].download_url; info.fileName = rel.files[0].filename; } return info; }); } -function getInfoFromDist(versionSpec) { +function getInfoFromDist(versionSpec, arch = os.arch()) { return __awaiter(this, void 0, void 0, function* () { let osPlat = os.platform(); - let osArch = translateArchToDistUrl(os.arch()); + let osArch = translateArchToDistUrl(arch); let version; - version = yield queryDistForMatch(versionSpec); + version = yield queryDistForMatch(versionSpec, arch); if (!version) { return null; } @@ -13241,14 +13252,15 @@ function getInfoFromDist(versionSpec) { return { downloadUrl: url, resolvedVersion: version, + arch: arch, fileName: fileName }; }); } -function resolveVersionFromManifest(versionSpec, stable, auth) { +function resolveVersionFromManifest(versionSpec, stable, auth, osArch = translateArchToDistUrl(os.arch())) { return __awaiter(this, void 0, void 0, function* () { try { - const info = yield getInfoFromManifest(versionSpec, stable, auth); + const info = yield getInfoFromManifest(versionSpec, stable, auth, osArch); return info === null || info === void 0 ? void 0 : info.resolvedVersion; } catch (err) { @@ -13283,10 +13295,10 @@ function evaluateVersions(versions, versionSpec) { } return version; } -function queryDistForMatch(versionSpec) { +function queryDistForMatch(versionSpec, arch = os.arch()) { return __awaiter(this, void 0, void 0, function* () { let osPlat = os.platform(); - let osArch = translateArchToDistUrl(os.arch()); + let osArch = translateArchToDistUrl(arch); // node offers a json list of versions let dataFileName; switch (osPlat) { @@ -13339,10 +13351,10 @@ exports.getVersionsFromDist = getVersionsFromDist; // This method attempts to download and cache the resources from these alternative locations. // Note also that the files are normally zipped but in this case they are just an exe // and lib file in a folder, not zipped. -function acquireNodeFromFallbackLocation(version) { +function acquireNodeFromFallbackLocation(version, arch = os.arch()) { return __awaiter(this, void 0, void 0, function* () { let osPlat = os.platform(); - let osArch = translateArchToDistUrl(os.arch()); + let osArch = translateArchToDistUrl(arch); // Create temporary folder to download in to const tempDownloadFolder = 'temp_' + Math.floor(Math.random() * 2000000000); const tempDirectory = process.env['RUNNER_TEMP'] || ''; @@ -13373,7 +13385,7 @@ function acquireNodeFromFallbackLocation(version) { throw err; } } - let toolPath = yield tc.cacheDir(tempDir, 'node', version); + let toolPath = yield tc.cacheDir(tempDir, 'node', version, arch); core.addPath(toolPath); return toolPath; }); diff --git a/src/installer.ts b/src/installer.ts index feb8349ce..cc45e2248 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -20,6 +20,7 @@ export interface INodeVersion { interface INodeVersionInfo { downloadUrl: string; resolvedVersion: string; + arch: string; fileName: string; } @@ -27,17 +28,19 @@ export async function getNode( versionSpec: string, stable: boolean, checkLatest: boolean, - auth: string | undefined + auth: string | undefined, + arch: string = os.arch() ) { let osPlat: string = os.platform(); - let osArch: string = translateArchToDistUrl(os.arch()); + let osArch: string = translateArchToDistUrl(arch); if (checkLatest) { core.info('Attempt to resolve the latest version from manifest...'); const resolvedVersion = await resolveVersionFromManifest( versionSpec, stable, - auth + auth, + osArch ); if (resolvedVersion) { versionSpec = resolvedVersion; @@ -49,7 +52,7 @@ export async function getNode( // check cache let toolPath: string; - toolPath = tc.find('node', versionSpec); + toolPath = tc.find('node', versionSpec, osArch); // If not found in cache, download if (toolPath) { @@ -63,9 +66,11 @@ export async function getNode( // Try download from internal distribution (popular versions only) // try { - info = await getInfoFromManifest(versionSpec, stable, auth); + info = await getInfoFromManifest(versionSpec, stable, auth, osArch); if (info) { - core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`); + core.info( + `Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}` + ); downloadPath = await tc.downloadTool(info.downloadUrl, undefined, auth); } else { core.info( @@ -92,19 +97,24 @@ export async function getNode( // Download from nodejs.org // if (!downloadPath) { - info = await getInfoFromDist(versionSpec); + info = await getInfoFromDist(versionSpec, arch); if (!info) { throw new Error( `Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.` ); } - core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`); + core.info( + `Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}` + ); try { downloadPath = await tc.downloadTool(info.downloadUrl); } catch (err) { if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { - return await acquireNodeFromFallbackLocation(info.resolvedVersion); + return await acquireNodeFromFallbackLocation( + info.resolvedVersion, + info.arch + ); } throw err; @@ -137,7 +147,12 @@ export async function getNode( // Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded // core.info('Adding to the cache ...'); - toolPath = await tc.cacheDir(extPath, 'node', info.resolvedVersion); + toolPath = await tc.cacheDir( + extPath, + 'node', + info.resolvedVersion, + info.arch + ); core.info('Done'); } @@ -158,7 +173,8 @@ export async function getNode( async function getInfoFromManifest( versionSpec: string, stable: boolean, - auth: string | undefined + auth: string | undefined, + osArch: string = translateArchToDistUrl(os.arch()) ): Promise { let info: INodeVersionInfo | null = null; const releases = await tc.getManifestFromRepo( @@ -167,11 +183,12 @@ async function getInfoFromManifest( auth, 'main' ); - const rel = await tc.findFromManifest(versionSpec, stable, releases); + const rel = await tc.findFromManifest(versionSpec, stable, releases, osArch); if (rel && rel.files.length > 0) { info = {}; info.resolvedVersion = rel.version; + info.arch = rel.files[0].arch; info.downloadUrl = rel.files[0].download_url; info.fileName = rel.files[0].filename; } @@ -180,14 +197,15 @@ async function getInfoFromManifest( } async function getInfoFromDist( - versionSpec: string + versionSpec: string, + arch: string = os.arch() ): Promise { let osPlat: string = os.platform(); - let osArch: string = translateArchToDistUrl(os.arch()); + let osArch: string = translateArchToDistUrl(arch); let version: string; - version = await queryDistForMatch(versionSpec); + version = await queryDistForMatch(versionSpec, arch); if (!version) { return null; } @@ -207,6 +225,7 @@ async function getInfoFromDist( return { downloadUrl: url, resolvedVersion: version, + arch: arch, fileName: fileName }; } @@ -214,10 +233,11 @@ async function getInfoFromDist( async function resolveVersionFromManifest( versionSpec: string, stable: boolean, - auth: string | undefined + auth: string | undefined, + osArch: string = translateArchToDistUrl(os.arch()) ): Promise { try { - const info = await getInfoFromManifest(versionSpec, stable, auth); + const info = await getInfoFromManifest(versionSpec, stable, auth, osArch); return info?.resolvedVersion; } catch (err) { core.info('Unable to resolve version from manifest...'); @@ -253,9 +273,12 @@ function evaluateVersions(versions: string[], versionSpec: string): string { return version; } -async function queryDistForMatch(versionSpec: string): Promise { +async function queryDistForMatch( + versionSpec: string, + arch: string = os.arch() +): Promise { let osPlat: string = os.platform(); - let osArch: string = translateArchToDistUrl(os.arch()); + let osArch: string = translateArchToDistUrl(arch); // node offers a json list of versions let dataFileName: string; @@ -311,10 +334,11 @@ export async function getVersionsFromDist(): Promise { // Note also that the files are normally zipped but in this case they are just an exe // and lib file in a folder, not zipped. async function acquireNodeFromFallbackLocation( - version: string + version: string, + arch: string = os.arch() ): Promise { let osPlat: string = os.platform(); - let osArch: string = translateArchToDistUrl(os.arch()); + let osArch: string = translateArchToDistUrl(arch); // Create temporary folder to download in to const tempDownloadFolder: string = @@ -348,7 +372,7 @@ async function acquireNodeFromFallbackLocation( throw err; } } - let toolPath = await tc.cacheDir(tempDir, 'node', version); + let toolPath = await tc.cacheDir(tempDir, 'node', version, arch); core.addPath(toolPath); return toolPath; } diff --git a/src/main.ts b/src/main.ts index 328db0d26..343327653 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import * as installer from './installer'; import * as auth from './authutil'; import * as path from 'path'; import {URL} from 'url'; +import os = require('os'); export async function run() { try { @@ -15,13 +16,27 @@ export async function run() { version = core.getInput('version'); } + let arch = core.getInput('architecture'); + + // if architecture supplied but node-version is not + // if we don't throw a warning, the already installed x64 node will be used which is not probably what user meant. + if (arch && !version) { + core.warning( + '`architecture` is provided but `node-version` is missing. In this configuration, the version/architecture of Node will not be changed. To fix this, provide `architecture` in combination with `node-version`' + ); + } + + if (!arch) { + arch = os.arch(); + } + if (version) { let token = core.getInput('token'); let auth = !token || isGhes() ? undefined : `token ${token}`; let stable = (core.getInput('stable') || 'true').toUpperCase() === 'TRUE'; const checkLatest = (core.getInput('check-latest') || 'false').toUpperCase() === 'TRUE'; - await installer.getNode(version, stable, checkLatest, auth); + await installer.getNode(version, stable, checkLatest, auth, arch); } const registryUrl: string = core.getInput('registry-url');