Skip to content

Commit 02574b1

Browse files
Add support for optional architecture input for cross-architecture .NET installs (#700)
* fix basic validation with npm command * Revert "fix basic validation with npm command" This reverts commit 27a0803. * add architecture support * updated installdir logic * update architecture resolution * update normalizeArch
1 parent 16c7b3c commit 02574b1

File tree

8 files changed

+401
-8
lines changed

8 files changed

+401
-8
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,3 +623,38 @@ jobs:
623623
- name: Verify dotnet
624624
shell: pwsh
625625
run: __tests__/verify-dotnet.ps1 -Patterns "^9\.0"
626+
627+
test-setup-with-architecture-input:
628+
runs-on: ${{ matrix.os }}
629+
strategy:
630+
fail-fast: false
631+
matrix:
632+
os: [macos-latest, ubuntu-latest, windows-latest]
633+
arch: [x64, arm64]
634+
exclude:
635+
- os: windows-latest
636+
arch: arm64
637+
- os: ubuntu-latest
638+
arch: arm64
639+
640+
steps:
641+
- name: Checkout
642+
uses: actions/checkout@v6
643+
644+
- name: Clear toolcache
645+
shell: pwsh
646+
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
647+
648+
- name: Setup dotnet (${{ matrix.arch }})
649+
uses: ./
650+
with:
651+
dotnet-version: |
652+
8.0.416
653+
8.0.x
654+
9.0.308
655+
10.0.101
656+
architecture: ${{ matrix.arch }}
657+
658+
- name: Verify dotnet
659+
shell: pwsh
660+
run: __tests__/verify-dotnet.ps1 -Patterns "^8.0.416$", "^9.0.308$", "^10.0.101$", "^8.0"

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ The `dotnet-version` input supports following syntax:
5959
- **A.B.Cxx** (e.g. 8.0.4xx) - available since `.NET 5.0` release. Installs the latest version of the specific SDK release, including prerelease versions (preview, rc).
6060

6161

62+
## Using the `architecture` input
63+
Using the architecture input, it is possible to specify the required .NET SDK architecture. Possible values: `x64`, `x86`, `arm64`, `amd64`, `arm`, `s390x`, `ppc64le`, `riscv64`. If the input is not specified, the architecture defaults to the host OS architecture (not all of the architectures are available on all platforms).
64+
65+
**Example: Install multiple SDK versions for a specific architecture**
66+
```yml
67+
steps:
68+
- uses: actions/checkout@v6
69+
- name: Setup dotnet (x86)
70+
uses: actions/setup-dotnet@v5
71+
with:
72+
dotnet-version: |
73+
8.0.x
74+
9.0.x
75+
architecture: x86
76+
- run: dotnet build <my project>
77+
```
78+
6279
## Using the `dotnet-quality` input
6380
This input sets up the action to install the latest build of the specified quality in the channel. The possible values of `dotnet-quality` are: **daily**, **signed**, **validated**, **preview**, **ga**.
6481

__tests__/installer.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import each from 'jest-each';
22
import semver from 'semver';
33
import fs from 'fs';
44
import fspromises from 'fs/promises';
5+
import os from 'os';
56
import * as exec from '@actions/exec';
67
import * as core from '@actions/core';
78
import * as io from '@actions/io';
@@ -327,6 +328,143 @@ describe('installer tests', () => {
327328
);
328329
});
329330
}
331+
332+
it(`should supply 'architecture' argument to the installation script when architecture is provided`, async () => {
333+
const inputVersion = '10.0.101';
334+
const inputQuality = '' as QualityOptions;
335+
const inputArchitecture = 'x64';
336+
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
337+
338+
getExecOutputSpy.mockImplementation(() => {
339+
return Promise.resolve({
340+
exitCode: 0,
341+
stdout: `${stdout}`,
342+
stderr: ''
343+
});
344+
});
345+
maxSatisfyingSpy.mockImplementation(() => inputVersion);
346+
347+
const dotnetInstaller = new installer.DotnetCoreInstaller(
348+
inputVersion,
349+
inputQuality,
350+
inputArchitecture
351+
);
352+
353+
await dotnetInstaller.installDotnet();
354+
355+
const callIndex = 1;
356+
const scriptArguments = (
357+
getExecOutputSpy.mock.calls[callIndex][1] as string[]
358+
).join(' ');
359+
const expectedArgument = IS_WINDOWS
360+
? `-Architecture ${inputArchitecture}`
361+
: `--architecture ${inputArchitecture}`;
362+
363+
expect(scriptArguments).toContain(expectedArgument);
364+
});
365+
366+
it(`should NOT supply 'architecture' argument when architecture is not provided`, async () => {
367+
const inputVersion = '10.0.101';
368+
const inputQuality = '' as QualityOptions;
369+
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
370+
371+
getExecOutputSpy.mockImplementation(() => {
372+
return Promise.resolve({
373+
exitCode: 0,
374+
stdout: `${stdout}`,
375+
stderr: ''
376+
});
377+
});
378+
maxSatisfyingSpy.mockImplementation(() => inputVersion);
379+
380+
const dotnetInstaller = new installer.DotnetCoreInstaller(
381+
inputVersion,
382+
inputQuality
383+
);
384+
385+
await dotnetInstaller.installDotnet();
386+
387+
const callIndex = 1;
388+
const scriptArguments = (
389+
getExecOutputSpy.mock.calls[callIndex][1] as string[]
390+
).join(' ');
391+
392+
expect(scriptArguments).not.toContain('--architecture');
393+
expect(scriptArguments).not.toContain('-Architecture');
394+
});
395+
396+
it(`should supply 'install-dir' with arch subdirectory for cross-arch install`, async () => {
397+
const inputVersion = '10.0.101';
398+
const inputQuality = '' as QualityOptions;
399+
const inputArchitecture = 'x64';
400+
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
401+
402+
getExecOutputSpy.mockImplementation(() => {
403+
return Promise.resolve({
404+
exitCode: 0,
405+
stdout: `${stdout}`,
406+
stderr: ''
407+
});
408+
});
409+
maxSatisfyingSpy.mockImplementation(() => inputVersion);
410+
411+
// Mock os.arch() to return a different arch to simulate cross-arch
412+
const archSpy = jest.spyOn(os, 'arch').mockReturnValue('arm64');
413+
414+
const dotnetInstaller = new installer.DotnetCoreInstaller(
415+
inputVersion,
416+
inputQuality,
417+
inputArchitecture
418+
);
419+
420+
await dotnetInstaller.installDotnet();
421+
422+
const callIndex = 1;
423+
const scriptArguments = (
424+
getExecOutputSpy.mock.calls[callIndex][1] as string[]
425+
).join(' ');
426+
427+
const expectedInstallDirFlag = IS_WINDOWS
428+
? '-InstallDir'
429+
: '--install-dir';
430+
431+
expect(scriptArguments).toContain(expectedInstallDirFlag);
432+
expect(scriptArguments).toContain(inputArchitecture);
433+
434+
archSpy.mockRestore();
435+
});
436+
437+
it(`should NOT supply 'install-dir' when architecture matches runner's native arch`, async () => {
438+
const inputVersion = '10.0.101';
439+
const inputQuality = '' as QualityOptions;
440+
const nativeArch = os.arch().toLowerCase();
441+
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
442+
443+
getExecOutputSpy.mockImplementation(() => {
444+
return Promise.resolve({
445+
exitCode: 0,
446+
stdout: `${stdout}`,
447+
stderr: ''
448+
});
449+
});
450+
maxSatisfyingSpy.mockImplementation(() => inputVersion);
451+
452+
const dotnetInstaller = new installer.DotnetCoreInstaller(
453+
inputVersion,
454+
inputQuality,
455+
nativeArch
456+
);
457+
458+
await dotnetInstaller.installDotnet();
459+
460+
const callIndex = 1;
461+
const scriptArguments = (
462+
getExecOutputSpy.mock.calls[callIndex][1] as string[]
463+
).join(' ');
464+
465+
expect(scriptArguments).not.toContain('--install-dir');
466+
expect(scriptArguments).not.toContain('-InstallDir');
467+
});
330468
});
331469

332470
describe('addToPath() tests', () => {
@@ -346,6 +484,32 @@ describe('installer tests', () => {
346484
});
347485
});
348486

487+
describe('normalizeArch() tests', () => {
488+
it(`should normalize 'amd64' to 'x64'`, () => {
489+
expect(installer.normalizeArch('amd64')).toBe('x64');
490+
});
491+
492+
it(`should normalize 'AMD64' to 'x64' (case-insensitive)`, () => {
493+
expect(installer.normalizeArch('AMD64')).toBe('x64');
494+
});
495+
496+
it(`should pass through 'x64' unchanged`, () => {
497+
expect(installer.normalizeArch('x64')).toBe('x64');
498+
});
499+
500+
it(`should pass through 'arm64' unchanged`, () => {
501+
expect(installer.normalizeArch('arm64')).toBe('arm64');
502+
});
503+
504+
it(`should lowercase 'ARM64'`, () => {
505+
expect(installer.normalizeArch('ARM64')).toBe('arm64');
506+
});
507+
508+
it(`should pass through 'x86' unchanged`, () => {
509+
expect(installer.normalizeArch('x86')).toBe('x86');
510+
});
511+
});
512+
349513
describe('DotnetVersionResolver tests', () => {
350514
describe('createDotnetVersion() tests', () => {
351515
each([

__tests__/setup-dotnet.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as core from '@actions/core';
22
import fs from 'fs';
33
import semver from 'semver';
44
import * as auth from '../src/authutil';
5-
5+
import os from 'os';
66
import * as setup from '../src/setup-dotnet';
77
import {DotnetCoreInstaller, DotnetInstallDir} from '../src/installer';
88
import * as cacheUtils from '../src/cache-utils';
@@ -221,5 +221,40 @@ describe('setup-dotnet tests', () => {
221221
await setup.run();
222222
expect(restoreCacheSpy).not.toHaveBeenCalled();
223223
});
224+
225+
it('should pass valid architecture input to DotnetCoreInstaller', async () => {
226+
inputs['dotnet-version'] = ['10.0.101'];
227+
inputs['dotnet-quality'] = '';
228+
inputs['architecture'] = os.arch().toLowerCase();
229+
230+
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
231+
232+
await setup.run();
233+
expect(installDotnetSpy).toHaveBeenCalledTimes(1);
234+
expect(DotnetInstallDir.addToPath).toHaveBeenCalledTimes(1);
235+
});
236+
237+
it('should work with empty architecture input for auto-detection', async () => {
238+
inputs['dotnet-version'] = ['10.0.101'];
239+
inputs['dotnet-quality'] = '';
240+
inputs['architecture'] = '';
241+
242+
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
243+
244+
await setup.run();
245+
expect(installDotnetSpy).toHaveBeenCalledTimes(1);
246+
expect(DotnetInstallDir.addToPath).toHaveBeenCalledTimes(1);
247+
});
248+
249+
it('should fail the action if unsupported architecture is provided', async () => {
250+
inputs['dotnet-version'] = ['10.0.101'];
251+
inputs['dotnet-quality'] = '';
252+
inputs['architecture'] = 'x688';
253+
254+
const expectedErrorMessage = `Value 'x688' is not supported for the 'architecture' option. Supported values are: x64, x86, arm64, amd64, arm, s390x, ppc64le, riscv64.`;
255+
256+
await setup.run();
257+
expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage);
258+
});
224259
});
225260
});

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ inputs:
2727
workloads:
2828
description: 'Optional SDK workloads to install for additional platform support. Examples: wasm-tools, maui, aspire.'
2929
required: false
30+
architecture:
31+
description: 'Optional architecture for the .NET install. Supported values: x64, x86, arm64, amd64, arm, s390x, ppc64le, riscv64. If not set, the installer auto-detects the current system architecture.'
32+
required: false
3033
outputs:
3134
cache-hit:
3235
description: 'A boolean value to indicate if a cache was hit.'

0 commit comments

Comments
 (0)