Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c62040958 |
No files matched your search
@@ -452,13 +452,7 @@ You can also set `DOTNET_INSTALL_DIR` to a value based on runtime variables, suc
|
|||||||
- name: Set DOTNET_INSTALL_DIR
|
- name: Set DOTNET_INSTALL_DIR
|
||||||
run: echo "DOTNET_INSTALL_DIR=$HOME/.dotnet" >> $GITHUB_ENV
|
run: echo "DOTNET_INSTALL_DIR=$HOME/.dotnet" >> $GITHUB_ENV
|
||||||
```
|
```
|
||||||
> **Note**: On some self-hosted, larger or containerized runners the default location is not writable by the current user, and the installation fails with a permission error. The action resolves the install directory in this order:
|
> **Note**: On some self-hosted or large Linux runners, installing .NET under the default `/usr/share/dotnet` location may fail due to insufficient permissions. To ensure successful installation, set `DOTNET_INSTALL_DIR` to a user-writable path.
|
||||||
>
|
|
||||||
> 1. `DOTNET_INSTALL_DIR`, when set. It is always honored and never probed.
|
|
||||||
> 2. The default location for the runner OS, when it is writable.
|
|
||||||
> 3. `.dotnet` in the current user's home directory, for example `/home/runner/.dotnet` or `C:\Users\runneradmin\.dotnet`.
|
|
||||||
>
|
|
||||||
> Runners that can write to the default location are unaffected. When the fallback is used, `DOTNET_ROOT` and `PATH` point at the home directory, so .NET preinstalled under the system location is no longer picked up. Set `DOTNET_INSTALL_DIR` explicitly to pin the location. If no writable location is found, the action warns and keeps the default, so the installation is likely to fail.
|
|
||||||
|
|
||||||
## Recommended permissions
|
## Recommended permissions
|
||||||
|
|
||||||
|
|||||||
+10
-266
@@ -11,7 +11,6 @@ import {
|
|||||||
import each from 'jest-each';
|
import each from 'jest-each';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import fspromises from 'fs/promises';
|
import fspromises from 'fs/promises';
|
||||||
import type {Stats} from 'fs';
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@@ -36,47 +35,26 @@ jest.unstable_mockModule('@actions/io', () => ({
|
|||||||
jest.unstable_mockModule('fs', () => {
|
jest.unstable_mockModule('fs', () => {
|
||||||
const actual = jest.requireActual('fs') as typeof import('fs');
|
const actual = jest.requireActual('fs') as typeof import('fs');
|
||||||
const chmodSync = jest.fn();
|
const chmodSync = jest.fn();
|
||||||
const lstatSync = jest.fn(actual.lstatSync);
|
|
||||||
const mkdtempSync = jest.fn(actual.mkdtempSync);
|
|
||||||
const rmSync = jest.fn(actual.rmSync);
|
|
||||||
const readdirSync = jest.fn();
|
const readdirSync = jest.fn();
|
||||||
const existsSync = jest.fn(() => true);
|
const existsSync = jest.fn(() => true);
|
||||||
const statSync = jest.fn(() => ({isFile: () => true}));
|
const statSync = jest.fn(() => ({isFile: () => true}));
|
||||||
const accessSync = jest.fn();
|
const accessSync = jest.fn();
|
||||||
const mocked = {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
chmodSync,
|
chmodSync,
|
||||||
lstatSync,
|
|
||||||
mkdtempSync,
|
|
||||||
rmSync,
|
|
||||||
readdirSync,
|
readdirSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
statSync,
|
statSync,
|
||||||
accessSync
|
accessSync,
|
||||||
|
default: {
|
||||||
|
...actual,
|
||||||
|
chmodSync,
|
||||||
|
readdirSync,
|
||||||
|
existsSync,
|
||||||
|
statSync,
|
||||||
|
accessSync
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return {
|
|
||||||
...mocked,
|
|
||||||
default: mocked
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.unstable_mockModule('os', () => {
|
|
||||||
const actual = jest.requireActual('os') as typeof import('os');
|
|
||||||
// os.homedir() reads the real environment, not jest's process.env.
|
|
||||||
const homedir = jest.fn(actual.homedir);
|
|
||||||
// Resolved per call, so spies placed on the real module still take effect.
|
|
||||||
const arch = () => actual.arch();
|
|
||||||
return {...actual, homedir, arch, default: {...actual, homedir, arch}};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Allows tests to re-import the installer as if running on another OS.
|
|
||||||
let platformOverride: 'windows' | 'linux' | 'mac' | undefined;
|
|
||||||
jest.unstable_mockModule('../src/utils.js', () => {
|
|
||||||
const actual = jest.requireActual(
|
|
||||||
'../src/utils.js'
|
|
||||||
) as typeof import('../src/utils.js');
|
|
||||||
const platform = platformOverride ?? actual.PLATFORM;
|
|
||||||
return {...actual, PLATFORM: platform, IS_WINDOWS: platform === 'windows'};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const exec = await import('@actions/exec');
|
const exec = await import('@actions/exec');
|
||||||
@@ -1386,240 +1364,6 @@ describe('installer tests', () => {
|
|||||||
expect(path).toContain(process.env['DOTNET_INSTALL_DIR']);
|
expect(path).toContain(process.env['DOTNET_INSTALL_DIR']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('install directory fallback tests', () => {
|
|
||||||
// Mock factories re-run on resetModules, so the installer sees new
|
|
||||||
// instances; configure those, not the ones captured at file load.
|
|
||||||
const importInstallerFor = async (
|
|
||||||
platform: 'windows' | 'linux' | 'mac',
|
|
||||||
isWritable: (dir: string) => boolean
|
|
||||||
) => {
|
|
||||||
platformOverride = platform;
|
|
||||||
jest.resetModules();
|
|
||||||
|
|
||||||
const freshFs = await import('fs');
|
|
||||||
(freshFs.lstatSync as jest.Mock).mockReturnValue({} as Stats);
|
|
||||||
(freshFs.mkdtempSync as jest.Mock).mockImplementation(
|
|
||||||
(...args: unknown[]) => {
|
|
||||||
const prefix = String(args[0]);
|
|
||||||
if (!isWritable(path.dirname(prefix))) {
|
|
||||||
throw Object.assign(new Error('permission denied'), {
|
|
||||||
code: 'EACCES'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return `${prefix}abc123`;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
(freshFs.rmSync as jest.Mock).mockImplementation(() => {});
|
|
||||||
|
|
||||||
const freshCore = await import('@actions/core');
|
|
||||||
const freshOs = await import('os');
|
|
||||||
const {DotnetInstallDir} = await import('../src/installer.js');
|
|
||||||
|
|
||||||
return {DotnetInstallDir, core: freshCore, fs: freshFs, os: freshOs};
|
|
||||||
};
|
|
||||||
|
|
||||||
const probedDirs = (freshFs: typeof import('fs')) =>
|
|
||||||
(freshFs.mkdtempSync as jest.Mock).mock.calls.map(call =>
|
|
||||||
path.dirname(String(call[0]))
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
delete process.env['DOTNET_INSTALL_DIR'];
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
platformOverride = undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should use the default location when it is writable`, async () => {
|
|
||||||
const {DotnetInstallDir} = await importInstallerFor(
|
|
||||||
'linux',
|
|
||||||
() => true
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should fall back to the home directory when the default location is not writable`, async () => {
|
|
||||||
const fallbackPath = path.join(os.homedir(), '.dotnet');
|
|
||||||
const {DotnetInstallDir, core: freshCore} = await importInstallerFor(
|
|
||||||
'linux',
|
|
||||||
dir => dir === fallbackPath
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe(fallbackPath);
|
|
||||||
expect(freshCore.warning).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should reject a dangling symlink instead of probing its writable parent`, async () => {
|
|
||||||
// Resolved, because that is the form the probe receives on every platform.
|
|
||||||
const linkPath = path.resolve('/usr/share/dotnet');
|
|
||||||
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
||||||
'linux',
|
|
||||||
() => true
|
|
||||||
);
|
|
||||||
// A dangling link exists for lstat, but nothing can be created under it.
|
|
||||||
(freshFs.mkdtempSync as jest.Mock).mockImplementation(
|
|
||||||
(...args: unknown[]) => {
|
|
||||||
if (path.dirname(String(args[0])) === linkPath) {
|
|
||||||
throw Object.assign(new Error('no such file or directory'), {
|
|
||||||
code: 'ENOENT'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return `${args[0]}abc123`;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe(
|
|
||||||
path.join(os.homedir(), '.dotnet')
|
|
||||||
);
|
|
||||||
const probed = probedDirs(freshFs);
|
|
||||||
expect(probed).toContain(linkPath);
|
|
||||||
expect(probed).not.toContain(path.dirname(linkPath));
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should probe the nearest existing parent when the directory is missing`, async () => {
|
|
||||||
const systemPath = path.resolve('/usr/share/dotnet');
|
|
||||||
const parentPath = path.dirname(systemPath);
|
|
||||||
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
||||||
'linux',
|
|
||||||
() => true
|
|
||||||
);
|
|
||||||
// Only the parent exists, so the walk has to climb before probing.
|
|
||||||
(freshFs.lstatSync as jest.Mock).mockImplementation(
|
|
||||||
(...args: unknown[]) =>
|
|
||||||
path.resolve(String(args[0])) === parentPath
|
|
||||||
? ({} as Stats)
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
|
|
||||||
const probed = probedDirs(freshFs);
|
|
||||||
expect(probed).toContain(parentPath);
|
|
||||||
expect(probed).not.toContain(systemPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should give up at the filesystem root when no ancestor exists`, async () => {
|
|
||||||
const systemPath = path.resolve('/usr/share/dotnet');
|
|
||||||
const homePath = path.join(os.homedir(), '.dotnet');
|
|
||||||
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
||||||
'linux',
|
|
||||||
() => true
|
|
||||||
);
|
|
||||||
// Nothing on the system path exists, all the way up to the root.
|
|
||||||
(freshFs.lstatSync as jest.Mock).mockImplementation(
|
|
||||||
(...args: unknown[]) =>
|
|
||||||
systemPath.startsWith(path.resolve(String(args[0])))
|
|
||||||
? undefined
|
|
||||||
: ({} as Stats)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe(homePath);
|
|
||||||
// The walk hit the root and returned without ever probing.
|
|
||||||
expect(probedDirs(freshFs)).toEqual([homePath]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should not probe a relative default location`, async () => {
|
|
||||||
// An unset HOME makes the macOS default relative, e.g. 'undefined/.dotnet'.
|
|
||||||
delete process.env['HOME'];
|
|
||||||
const homePath = path.join(os.homedir(), '.dotnet');
|
|
||||||
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
||||||
'mac',
|
|
||||||
() => true
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe(homePath);
|
|
||||||
expect(probedDirs(freshFs)).not.toContain(process.cwd());
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should not probe a relative default location on Windows`, async () => {
|
|
||||||
// An unset PROGRAMFILES makes the Windows default relative.
|
|
||||||
delete process.env['PROGRAMFILES'];
|
|
||||||
const homePath = path.join(os.homedir(), '.dotnet');
|
|
||||||
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
||||||
'windows',
|
|
||||||
() => true
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe(homePath);
|
|
||||||
expect(probedDirs(freshFs)).not.toContain(process.cwd());
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should not treat the filesystem root as a home directory`, async () => {
|
|
||||||
const root = path.parse(process.cwd()).root;
|
|
||||||
const rootDotnet = path.join(root, '.dotnet');
|
|
||||||
// Only the root candidate is writable, so an unguarded run would pick it.
|
|
||||||
const {
|
|
||||||
DotnetInstallDir,
|
|
||||||
os: freshOs,
|
|
||||||
core: freshCore
|
|
||||||
} = await importInstallerFor('linux', dir => dir === rootDotnet);
|
|
||||||
(freshOs.homedir as jest.Mock).mockReturnValue(root);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
|
|
||||||
expect(freshCore.warning).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should still report writable when the probe cannot be removed`, async () => {
|
|
||||||
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
||||||
'linux',
|
|
||||||
() => true
|
|
||||||
);
|
|
||||||
(freshFs.rmSync as jest.Mock).mockImplementation(() => {
|
|
||||||
throw Object.assign(new Error('device or resource busy'), {
|
|
||||||
code: 'EBUSY'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should keep the shared macOS location and warn when it is not writable`, async () => {
|
|
||||||
// The mac default derives from HOME, homeInstallPath() from os.homedir();
|
|
||||||
// aligning them is what makes both candidates the same path.
|
|
||||||
process.env['HOME'] = os.homedir();
|
|
||||||
const homePath = path.join(os.homedir(), '.dotnet');
|
|
||||||
const {
|
|
||||||
DotnetInstallDir,
|
|
||||||
fs: freshFs,
|
|
||||||
core: freshCore
|
|
||||||
} = await importInstallerFor('mac', () => false);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe(homePath);
|
|
||||||
// One candidate, so it is probed once rather than twice.
|
|
||||||
expect(probedDirs(freshFs)).toEqual([homePath]);
|
|
||||||
expect(freshCore.warning).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should prefer DOTNET_INSTALL_DIR env.var without probing`, async () => {
|
|
||||||
process.env['DOTNET_INSTALL_DIR'] = path.join(path.sep, 'custom');
|
|
||||||
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
||||||
'linux',
|
|
||||||
() => false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(DotnetInstallDir.dirPath).toBe(path.join(path.sep, 'custom'));
|
|
||||||
expect(freshFs.mkdtempSync as jest.Mock).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should resolve once, and not before the directory is needed`, async () => {
|
|
||||||
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
||||||
'linux',
|
|
||||||
() => true
|
|
||||||
);
|
|
||||||
const probe = freshFs.mkdtempSync as jest.Mock;
|
|
||||||
|
|
||||||
expect(probe).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
void DotnetInstallDir.dirPath;
|
|
||||||
const afterFirstRead = probe.mock.calls.length;
|
|
||||||
void DotnetInstallDir.dirPath;
|
|
||||||
|
|
||||||
expect(afterFirstRead).toBeGreaterThan(0);
|
|
||||||
expect(probe.mock.calls.length).toBe(afterFirstRead);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('normalizeArch() tests', () => {
|
describe('normalizeArch() tests', () => {
|
||||||
|
|||||||
Vendored
+6
-80
@@ -44195,84 +44195,9 @@ class DotnetInstallDir {
|
|||||||
mac: external_path_default().join(process.env['HOME'] + '', '.dotnet'),
|
mac: external_path_default().join(process.env['HOME'] + '', '.dotnet'),
|
||||||
windows: external_path_default().join(process.env['PROGRAMFILES'] + '', 'dotnet')
|
windows: external_path_default().join(process.env['PROGRAMFILES'] + '', 'dotnet')
|
||||||
};
|
};
|
||||||
static resolvedDirPath;
|
static dirPath = process.env['DOTNET_INSTALL_DIR']
|
||||||
// Resolved on first use so a job that installs nothing never touches the disk.
|
? DotnetInstallDir.convertInstallPathToAbsolute(process.env['DOTNET_INSTALL_DIR'])
|
||||||
static get dirPath() {
|
: DotnetInstallDir.default[PLATFORM];
|
||||||
DotnetInstallDir.resolvedDirPath ??= DotnetInstallDir.resolveDirPath();
|
|
||||||
return DotnetInstallDir.resolvedDirPath;
|
|
||||||
}
|
|
||||||
static resolveDirPath() {
|
|
||||||
if (process.env['DOTNET_INSTALL_DIR']) {
|
|
||||||
return DotnetInstallDir.convertInstallPathToAbsolute(process.env['DOTNET_INSTALL_DIR']);
|
|
||||||
}
|
|
||||||
const systemPath = DotnetInstallDir.default[PLATFORM];
|
|
||||||
const homePath = DotnetInstallDir.homeInstallPath();
|
|
||||||
// A relative default (unset HOME/PROGRAMFILES) would probe the cwd.
|
|
||||||
if (external_path_default().isAbsolute(systemPath) &&
|
|
||||||
DotnetInstallDir.isWritableLocation(systemPath)) {
|
|
||||||
return systemPath;
|
|
||||||
}
|
|
||||||
// On macOS both candidates are the same path, already probed above.
|
|
||||||
const hasFallback = !!homePath && homePath !== systemPath;
|
|
||||||
if (hasFallback && DotnetInstallDir.isWritableLocation(homePath)) {
|
|
||||||
warning(`The default .NET install directory '${systemPath}' is not writable by the current user. Falling back to '${homePath}'; .NET preinstalled in the default location will no longer be used. Set the DOTNET_INSTALL_DIR environment variable to override this location.`);
|
|
||||||
return homePath;
|
|
||||||
}
|
|
||||||
// Not setFailed: the install may still succeed, so let it report the error.
|
|
||||||
warning(hasFallback
|
|
||||||
? `Neither the default .NET install directory '${systemPath}' nor '${homePath}' is writable by the current user. Keeping '${systemPath}', but the installation is likely to fail. Set the DOTNET_INSTALL_DIR environment variable to a writable location.`
|
|
||||||
: `The default .NET install directory '${systemPath}' is not writable by the current user. Keeping it, but the installation is likely to fail. Set the DOTNET_INSTALL_DIR environment variable to a writable location.`);
|
|
||||||
return systemPath;
|
|
||||||
}
|
|
||||||
static homeInstallPath() {
|
|
||||||
try {
|
|
||||||
const home = external_os_default().homedir();
|
|
||||||
// An empty HOME resolves against the cwd; a root HOME gives '/.dotnet'.
|
|
||||||
if (!external_path_default().isAbsolute(home) || home === external_path_default().parse(home).root) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return external_path_default().join(home, '.dotnet');
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Writability is tested by creating a directory. accessSync is not enough: it
|
|
||||||
// ignores Windows ACLs, so it reports success where the install would fail.
|
|
||||||
static isWritableLocation(installDir) {
|
|
||||||
let existingPath = external_path_default().resolve(installDir);
|
|
||||||
try {
|
|
||||||
// lstat also matches a broken symlink. existsSync does not, and the walk
|
|
||||||
// would skip past it to a writable parent and wrongly report success.
|
|
||||||
while (!(0,external_fs_namespaceObject.lstatSync)(existingPath, { throwIfNoEntry: false })) {
|
|
||||||
const parentPath = external_path_default().dirname(existingPath);
|
|
||||||
if (parentPath === existingPath)
|
|
||||||
return false;
|
|
||||||
existingPath = parentPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let probeDir;
|
|
||||||
try {
|
|
||||||
probeDir = (0,external_fs_namespaceObject.mkdtempSync)(external_path_default().join(existingPath, '.setup-dotnet-probe-'));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if (probeDir) {
|
|
||||||
try {
|
|
||||||
(0,external_fs_namespaceObject.rmSync)(probeDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Throwing here would discard the result already returned above.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static convertInstallPathToAbsolute(installDir) {
|
static convertInstallPathToAbsolute(installDir) {
|
||||||
if (external_path_default().isAbsolute(installDir))
|
if (external_path_default().isAbsolute(installDir))
|
||||||
return external_path_default().normalize(installDir);
|
return external_path_default().normalize(installDir);
|
||||||
@@ -44332,6 +44257,9 @@ class DotnetCoreInstaller {
|
|||||||
rollForward;
|
rollForward;
|
||||||
skipLtsPrepass;
|
skipLtsPrepass;
|
||||||
static FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/;
|
static FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/;
|
||||||
|
static {
|
||||||
|
DotnetInstallDir.setEnvironmentVariable();
|
||||||
|
}
|
||||||
constructor(version, quality, architecture, dotnetChannel, checkLatest = true, minimumVersion, rollForward, skipLtsPrepass = false) {
|
constructor(version, quality, architecture, dotnetChannel, checkLatest = true, minimumVersion, rollForward, skipLtsPrepass = false) {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.quality = quality;
|
this.quality = quality;
|
||||||
@@ -44519,8 +44447,6 @@ class DotnetCoreInstaller {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
async installDotnet() {
|
async installDotnet() {
|
||||||
// Before findLocalSdkVersion(), which looks under the resolved directory.
|
|
||||||
DotnetInstallDir.setEnvironmentVariable();
|
|
||||||
const isCrossArch = !!this.architecture &&
|
const isCrossArch = !!this.architecture &&
|
||||||
normalizeArch(this.architecture) !== normalizeArch(external_os_default().arch());
|
normalizeArch(this.architecture) !== normalizeArch(external_os_default().arch());
|
||||||
if (!this.checkLatest && !isCrossArch) {
|
if (!this.checkLatest && !isCrossArch) {
|
||||||
|
|||||||
+8
-96
@@ -7,10 +7,7 @@ import {
|
|||||||
accessSync,
|
accessSync,
|
||||||
chmodSync,
|
chmodSync,
|
||||||
constants as fsConstants,
|
constants as fsConstants,
|
||||||
lstatSync,
|
|
||||||
mkdtempSync,
|
|
||||||
readdirSync,
|
readdirSync,
|
||||||
rmSync,
|
|
||||||
statSync
|
statSync
|
||||||
} from 'fs';
|
} from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -350,97 +347,11 @@ export abstract class DotnetInstallDir {
|
|||||||
windows: path.join(process.env['PROGRAMFILES'] + '', 'dotnet')
|
windows: path.join(process.env['PROGRAMFILES'] + '', 'dotnet')
|
||||||
};
|
};
|
||||||
|
|
||||||
private static resolvedDirPath: string | undefined;
|
public static readonly dirPath = process.env['DOTNET_INSTALL_DIR']
|
||||||
|
? DotnetInstallDir.convertInstallPathToAbsolute(
|
||||||
// Resolved on first use so a job that installs nothing never touches the disk.
|
|
||||||
public static get dirPath(): string {
|
|
||||||
DotnetInstallDir.resolvedDirPath ??= DotnetInstallDir.resolveDirPath();
|
|
||||||
return DotnetInstallDir.resolvedDirPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static resolveDirPath(): string {
|
|
||||||
if (process.env['DOTNET_INSTALL_DIR']) {
|
|
||||||
return DotnetInstallDir.convertInstallPathToAbsolute(
|
|
||||||
process.env['DOTNET_INSTALL_DIR']
|
process.env['DOTNET_INSTALL_DIR']
|
||||||
);
|
)
|
||||||
}
|
: DotnetInstallDir.default[PLATFORM];
|
||||||
|
|
||||||
const systemPath = DotnetInstallDir.default[PLATFORM];
|
|
||||||
const homePath = DotnetInstallDir.homeInstallPath();
|
|
||||||
|
|
||||||
// A relative default (unset HOME/PROGRAMFILES) would probe the cwd.
|
|
||||||
if (
|
|
||||||
path.isAbsolute(systemPath) &&
|
|
||||||
DotnetInstallDir.isWritableLocation(systemPath)
|
|
||||||
) {
|
|
||||||
return systemPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On macOS both candidates are the same path, already probed above.
|
|
||||||
const hasFallback = !!homePath && homePath !== systemPath;
|
|
||||||
|
|
||||||
if (hasFallback && DotnetInstallDir.isWritableLocation(homePath!)) {
|
|
||||||
core.warning(
|
|
||||||
`The default .NET install directory '${systemPath}' is not writable by the current user. Falling back to '${homePath}'; .NET preinstalled in the default location will no longer be used. Set the DOTNET_INSTALL_DIR environment variable to override this location.`
|
|
||||||
);
|
|
||||||
return homePath!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not setFailed: the install may still succeed, so let it report the error.
|
|
||||||
core.warning(
|
|
||||||
hasFallback
|
|
||||||
? `Neither the default .NET install directory '${systemPath}' nor '${homePath}' is writable by the current user. Keeping '${systemPath}', but the installation is likely to fail. Set the DOTNET_INSTALL_DIR environment variable to a writable location.`
|
|
||||||
: `The default .NET install directory '${systemPath}' is not writable by the current user. Keeping it, but the installation is likely to fail. Set the DOTNET_INSTALL_DIR environment variable to a writable location.`
|
|
||||||
);
|
|
||||||
return systemPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static homeInstallPath(): string | undefined {
|
|
||||||
try {
|
|
||||||
const home = os.homedir();
|
|
||||||
// An empty HOME resolves against the cwd; a root HOME gives '/.dotnet'.
|
|
||||||
if (!path.isAbsolute(home) || home === path.parse(home).root) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return path.join(home, '.dotnet');
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writability is tested by creating a directory. accessSync is not enough: it
|
|
||||||
// ignores Windows ACLs, so it reports success where the install would fail.
|
|
||||||
private static isWritableLocation(installDir: string): boolean {
|
|
||||||
let existingPath = path.resolve(installDir);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// lstat also matches a broken symlink. existsSync does not, and the walk
|
|
||||||
// would skip past it to a writable parent and wrongly report success.
|
|
||||||
while (!lstatSync(existingPath, {throwIfNoEntry: false})) {
|
|
||||||
const parentPath = path.dirname(existingPath);
|
|
||||||
if (parentPath === existingPath) return false;
|
|
||||||
existingPath = parentPath;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let probeDir: string | undefined;
|
|
||||||
try {
|
|
||||||
probeDir = mkdtempSync(path.join(existingPath, '.setup-dotnet-probe-'));
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
if (probeDir) {
|
|
||||||
try {
|
|
||||||
rmSync(probeDir, {recursive: true, force: true});
|
|
||||||
} catch {
|
|
||||||
// Throwing here would discard the result already returned above.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static convertInstallPathToAbsolute(installDir: string): string {
|
private static convertInstallPathToAbsolute(installDir: string): string {
|
||||||
if (path.isAbsolute(installDir)) return path.normalize(installDir);
|
if (path.isAbsolute(installDir)) return path.normalize(installDir);
|
||||||
@@ -499,6 +410,10 @@ function isExecutableFile(filePath: string): boolean {
|
|||||||
export class DotnetCoreInstaller {
|
export class DotnetCoreInstaller {
|
||||||
private static readonly FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/;
|
private static readonly FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/;
|
||||||
|
|
||||||
|
static {
|
||||||
|
DotnetInstallDir.setEnvironmentVariable();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private version: string,
|
private version: string,
|
||||||
private quality: QualityOptions,
|
private quality: QualityOptions,
|
||||||
@@ -772,9 +687,6 @@ export class DotnetCoreInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async installDotnet(): Promise<string | null> {
|
public async installDotnet(): Promise<string | null> {
|
||||||
// Before findLocalSdkVersion(), which looks under the resolved directory.
|
|
||||||
DotnetInstallDir.setEnvironmentVariable();
|
|
||||||
|
|
||||||
const isCrossArch =
|
const isCrossArch =
|
||||||
!!this.architecture &&
|
!!this.architecture &&
|
||||||
normalizeArch(this.architecture) !== normalizeArch(os.arch());
|
normalizeArch(this.architecture) !== normalizeArch(os.arch());
|
||||||
|
|||||||
Reference in new issue
Block a user