Skip to content

Commit 9c0a49f

Browse files
authored
Use ELF/COFF/PE/MACHO Header to get Executable Architecture (#2309)
* Use ELF/COFF/PE/MACHO Header to get Executable Architecture We need to verify a user's install matches the architecture that the code application / extension wants to run under. We used to do this with dotnet --info which is a fallback. Now, we can also do this with dotnet --list-runtimes --arch when supported. However, checking if it is supported requires an extra process call which is slower than reading the file header. Once we hit dotnet 11, we will know that --arch is supported so we can skip this work of reading the file header. For now, this will allow us to gain a perf boost in the cases where this works. Documented in the code is how this logic works. Additionally, I've added test executables (real .net 9 ones) to show it works. Finally, I added the copyfiles module to copy the test assets into the dist folder, as otherwise they dont get copied. * Update package lock to include copy file in a separate commit so easy to revert /etc * Fix merge conflict * Fix typo * Fix lint * Fix tests * Fix tests more * Speed up test by mocking list sdks * Fix other tests * Fix comments
1 parent 58f8f73 commit 9c0a49f

24 files changed

+7964
-7461
lines changed

sample/yarn.lock

Lines changed: 2226 additions & 2226 deletions
Large diffs are not rendered by default.

vscode-dotnet-runtime-extension/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,12 +424,12 @@ suite('DotnetCoreAcquisitionExtension End to End', function ()
424424
}).timeout(standardTimeoutTime);
425425

426426

427-
test('Find dotnet PATH Command No Arch Available But Accept By Default', async () =>
427+
test('Find dotnet PATH Command No Arch Available But Arch Found From File', async () =>
428428
{
429429
// look for a different architecture of 3.1
430430
if (os.platform() !== 'darwin')
431431
{
432-
await findPathWithRequirementAndInstall('3.1', 'runtime', os.arch() == 'arm64' ? 'x64' : os.arch(), 'greater_than_or_equal', true,
432+
await findPathWithRequirementAndInstall('3.1', 'runtime', os.arch() == 'arm64' ? 'x64' : os.arch(), 'greater_than_or_equal', false,
433433
{ version: '3.1', mode: 'runtime', architecture: 'arm64', requestingExtensionId: requestingExtensionId }
434434
);
435435
}
@@ -623,7 +623,7 @@ the fake dotnet path setting is an empty dir -- if it is not empty, test cleanup
623623

624624
// acquire with the alternative extension id which has a path setting set to the fake path
625625
// If the setting is bad then it should also acquire somewhere else.
626-
const context: IDotnetAcquireContext = { version: '5.0', requestingExtensionId: 'alternative.extension', architecture: os.platform() };
626+
const context: IDotnetAcquireContext = { version: '5.0', requestingExtensionId: 'alternative.extension', architecture: os.arch() };
627627

628628
const resultForAcquiringPathSettingRuntime = await vscode.commands.executeCommand<IDotnetAcquireResult>('dotnet.acquire', context);
629629
assert.exists(resultForAcquiringPathSettingRuntime!.dotnetPath, 'Basic acquire works');

vscode-dotnet-runtime-extension/yarn.lock

Lines changed: 1976 additions & 1976 deletions
Large diffs are not rendered by default.

vscode-dotnet-runtime-library/package-lock.json

Lines changed: 103 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vscode-dotnet-runtime-library/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@
1616
"vscode:prepublish": "npm run compile && dotnet build ../msbuild/signJs --property jsOutputPath=..\\..\\vscode-dotnet-runtime-library\\dist -bl -v:normal",
1717
"compile": "npm run clean && tsc -p ./ && npm run mockPack",
1818
"mockPack": "run-script-os",
19+
"copyTestAssets": "copyfiles -u 4 \"src/test/mocks/Executables/**/*\" dist/test/mocks/Executables/",
1920
"mockPack:darwin:linux": "cd .. && sh ./mock-webpack.sh && cd vscode-dotnet-runtime-library",
2021
"mockPack:win32": "pushd .. && @powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./mock-webpack.ps1 && popd",
2122
"watch": "npm run clean && tsc -watch -p ./",
22-
"test": "npm run compile --silent && npm run unit-test",
23+
"test": "npm run compile --silent && npm run copyTestAssets && npm run unit-test",
2324
"unit-test": "mocha -u tdd -- dist/test/unit/**.test.js",
2425
"clean": "rimraf dist"
2526
},
2627
"devDependencies": {
2728
"@types/chai": "4.2.22",
2829
"@types/lodash": "^4.17.13",
2930
"@types/proper-lockfile": "^4.1.2",
31+
"copyfiles": "^2.4.1",
3032
"glob": "^7.2.0"
3133
},
3234
"dependencies": {

vscode-dotnet-runtime-library/src/Acquisition/DotnetConditionValidator.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DotnetConditionsValidated, DotnetFindPathDidNotMeetCondition, DotnetUna
66
import { IDotnetFindPathContext } from '../IDotnetFindPathContext';
77
import { CommandExecutor } from '../Utils/CommandExecutor';
88
import { CommandExecutorCommand } from '../Utils/CommandExecutorCommand';
9+
import { ExecutableArchitectureDetector } from '../Utils/ExecutableArchitectureDetector';
910
import { FileUtilities } from '../Utils/FileUtilities';
1011
import { ICommandExecutor } from '../Utils/ICommandExecutor';
1112
import { IUtilityContext } from '../Utils/IUtilityContext';
@@ -29,7 +30,7 @@ export class DotnetConditionValidator implements IDotnetConditionValidator
2930

3031
public async dotnetMeetsRequirement(dotnetExecutablePath: string, requirement: IDotnetFindPathContext): Promise<boolean>
3132
{
32-
let hostArch = '';
33+
const hostArch = new ExecutableArchitectureDetector().getExecutableArchitecture(dotnetExecutablePath);
3334
const oldLookup = process.env.DOTNET_MULTILEVEL_LOOKUP;
3435
// This is deprecated but still needed to scan .NET 6 and below
3536
process.env.DOTNET_MULTILEVEL_LOOKUP = '0'; // make it so --list-runtimes only finds the runtimes on that path: https://learn.microsoft.com/en-us/dotnet/core/compatibility/deployment/7.0/multilevel-lookup#reason-for-change
@@ -38,11 +39,11 @@ export class DotnetConditionValidator implements IDotnetConditionValidator
3839
{
3940
if (requirement.acquireContext.mode === 'sdk')
4041
{
41-
const availableSDKs = await this.getSDKs(dotnetExecutablePath, requirement.acquireContext.architecture ?? DotnetCoreAcquisitionWorker.defaultArchitecture());
42-
hostArch = availableSDKs?.at(0)?.architecture ?? await this.getHostArchitecture(dotnetExecutablePath, requirement);
42+
const availableSDKs = await this.getSDKs(dotnetExecutablePath, requirement.acquireContext.architecture ?? DotnetCoreAcquisitionWorker.defaultArchitecture(), ExecutableArchitectureDetector.IsKnownArchitecture(hostArch));
43+
const finalHostArch = ExecutableArchitectureDetector.IsKnownArchitecture(hostArch) ? hostArch : availableSDKs?.at(0)?.architecture ?? await this.getHostArchitecture(dotnetExecutablePath, requirement);
4344
if (availableSDKs.some((sdk) =>
4445
{
45-
return this.stringArchitectureMeetsRequirement(hostArch, requirement.acquireContext.architecture) &&
46+
return this.stringArchitectureMeetsRequirement(finalHostArch!, requirement.acquireContext.architecture) &&
4647
this.stringVersionMeetsRequirement(sdk.version, requirement.acquireContext.version, requirement) && this.allowPreview(sdk.version, requirement);
4748
}))
4849
{
@@ -53,11 +54,11 @@ export class DotnetConditionValidator implements IDotnetConditionValidator
5354
else
5455
{
5556
// No need to consider SDKs when looking for runtimes as all the runtimes installed with the SDKs will be included in the runtimes list.
56-
const availableRuntimes = await this.getRuntimes(dotnetExecutablePath, requirement.acquireContext.architecture ?? DotnetCoreAcquisitionWorker.defaultArchitecture());
57-
hostArch = availableRuntimes?.at(0)?.architecture ?? await this.getHostArchitecture(dotnetExecutablePath, requirement);
57+
const availableRuntimes = await this.getRuntimes(dotnetExecutablePath, requirement.acquireContext.architecture ?? DotnetCoreAcquisitionWorker.defaultArchitecture(), ExecutableArchitectureDetector.IsKnownArchitecture(hostArch));
58+
const finalHostArch = ExecutableArchitectureDetector.IsKnownArchitecture(hostArch) ? hostArch : availableRuntimes?.at(0)?.architecture ?? await this.getHostArchitecture(dotnetExecutablePath, requirement);
5859
if (availableRuntimes.some((runtime) =>
5960
{
60-
return runtime.mode === requirement.acquireContext.mode && this.stringArchitectureMeetsRequirement(hostArch, requirement.acquireContext.architecture) &&
61+
return runtime.mode === requirement.acquireContext.mode && this.stringArchitectureMeetsRequirement(finalHostArch!, requirement.acquireContext.architecture) &&
6162
this.stringVersionMeetsRequirement(runtime.version, requirement.acquireContext.version, requirement) && this.allowPreview(runtime.version, requirement);
6263
}))
6364
{
@@ -135,7 +136,7 @@ Please set the PATH to a dotnet host that matches the architecture ${requirement
135136
return hostArch;
136137
}
137138

138-
public async getSDKs(existingPath: string, requestedArchitecture: string): Promise<IDotnetListInfo[]>
139+
public async getSDKs(existingPath: string, requestedArchitecture: string, knownArchitecture: boolean): Promise<IDotnetListInfo[]>
139140
{
140141
if (!existingPath || existingPath === '""')
141142
{
@@ -151,7 +152,7 @@ Please set the PATH to a dotnet host that matches the architecture ${requirement
151152
return [];
152153
}
153154

154-
const hostSupportsArchFlag = await this.hostSupportsArchFlag(existingPath, result.stdout);
155+
const architectureKnown = knownArchitecture ? true : await this.hostSupportsArchFlag(existingPath, result.stdout);
155156
const sdks = result.stdout.split('\n').map((line) => line.trim()).filter((line) => (line?.length ?? 0) > 0);
156157
const sdkInfos: IDotnetListInfo[] = sdks.map((sdk) =>
157158
{
@@ -160,7 +161,7 @@ Please set the PATH to a dotnet host that matches the architecture ${requirement
160161
mode: 'sdk',
161162
version: parts[0],
162163
directory: sdk.split(' ').slice(1).join(' ').slice(1, -1), // need to remove the brackets from the path [path],
163-
architecture: hostSupportsArchFlag ? requestedArchitecture : null
164+
architecture: architectureKnown ? requestedArchitecture : null
164165
} as IDotnetListInfo;
165166
}).filter(x => x !== null) as IDotnetListInfo[];
166167

@@ -327,7 +328,7 @@ Please set the PATH to a dotnet host that matches the architecture ${requirement
327328
return CommandExecutor.makeCommand(`"${existingPath}"`, ['--list-runtimes', '--arch', requestedArchitecture]);
328329
}
329330

330-
public async getRuntimes(existingPath: string, requestedArchitecture: string | null): Promise<IDotnetListInfo[]>
331+
public async getRuntimes(existingPath: string, requestedArchitecture: string | null, knownArchitecture: boolean): Promise<IDotnetListInfo[]>
331332
{
332333
if (!existingPath || existingPath === '""')
333334
{
@@ -347,7 +348,7 @@ Please set the PATH to a dotnet host that matches the architecture ${requirement
347348
return [];
348349
}
349350

350-
const hostSupportsArchFlag = await this.hostSupportsArchFlag(existingPath, result.stdout);
351+
const architectureKnown = knownArchitecture ? true : await this.hostSupportsArchFlag(existingPath, result.stdout);
351352
const runtimes = result.stdout.split('\n').map((line) => line.trim()).filter((line) => (line?.length ?? 0) > 0);
352353
const runtimeInfos: IDotnetListInfo[] = runtimes.map((runtime) =>
353354
{
@@ -357,7 +358,7 @@ Please set the PATH to a dotnet host that matches the architecture ${requirement
357358
version: parts[1],
358359
directory: runtime.split(' ').slice(2).join(' ').slice(1, -1), // account for spaces in PATH, no space should appear before then and luckily path is last.
359360
// the 2nd slice needs to remove the brackets from the path [path]
360-
architecture: hostSupportsArchFlag ? requestedArchitecture : null
361+
architecture: architectureKnown ? requestedArchitecture : null
361362
} as IDotnetListInfo;
362363
}).filter(x => x !== null) as IDotnetListInfo[];
363364

vscode-dotnet-runtime-library/src/Acquisition/DotnetPathFinder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ export class DotnetPathFinder implements IDotnetPathFinder
388388
{
389389
// This will even work if only the sdk is installed, list-runtimes on an sdk installed host would work
390390
const validator = new DotnetConditionValidator(this.workerContext, this.utilityContext, this.executor);
391-
const runtimeInfo = await validator.getRuntimes(tentativePath, requestedArchitecture);
391+
const runtimeInfo = await validator.getRuntimes(tentativePath, requestedArchitecture, true);
392392
if ((runtimeInfo?.length ?? 0) > 0)
393393
{
394394
// q.t. from @dibarbet on the C# Extension:

vscode-dotnet-runtime-library/src/LocalMemoryCacheSingleton.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ export class LocalMemoryCacheSingleton
136136
let optionsString = '{';
137137
sortedKeys.forEach((k, index) =>
138138
{
139-
const v = key.options[k];
139+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
140+
const v = key.options?.[k];
140141
// Apply same replacer logic
141142
let value = v;
142143
if (k === 'dotnetInstallToolCacheTtlMs')

0 commit comments

Comments
 (0)