Skip to content

Commit 4122929

Browse files
authored
feat: [OSM-2830] Check for supported SDK project (#92)
* feat: [OSM-2830] Check for supported SDK project * feat: [OSM-2830] expose project SDK parsing * feat: [OSM-2830] Clarified project SDK to avoid confusion with dotnet SDK * feat: [OSM-2830] v2 requires net < 5 without dot
1 parent e5adaa1 commit 4122929

File tree

3 files changed

+231
-0
lines changed

3 files changed

+231
-0
lines changed

lib/index.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getTargetFrameworksFromProjectConfig,
1414
getTargetFrameworksFromProjectFile,
1515
getTargetFrameworksFromProjectJson,
16+
getSdkFromProjectFile,
1617
parseXmlFile,
1718
PkgTree,
1819
ProjectJsonManifest,
@@ -33,6 +34,7 @@ export {
3334
buildDepTreeFromProjectAssetsJson,
3435
buildDepTreeFromFiles,
3536
containsPackageReference,
37+
extractProjectSdkFromProjectFile,
3638
extractTargetFrameworksFromFiles,
3739
extractTargetFrameworksFromProjectFile,
3840
extractTargetFrameworksFromProjectConfig,
@@ -41,6 +43,7 @@ export {
4143
extractTargetSdkFromGlobalJson,
4244
extractProps,
4345
isSupportedByV2GraphGeneration,
46+
isSupportedByV3GraphGeneration,
4447
PkgTree,
4548
DepType,
4649
};
@@ -163,13 +166,54 @@ function isSupportedByV2GraphGeneration(targetFramework: string): boolean {
163166
// - .NET Framework: netNNN (unsupported)
164167
// So if there's a dot, we're good.
165168
if (targetFramework.includes('.')) {
169+
// Ensure that if it's "netN.N", we don't accept anything below 4.0.
170+
// It's not valid to supply something below 5 with dots (i.e. net4.8, should be net48 per the documentation
171+
// links above), but it's an easy mistake to make, and it's still accepted by the dotnet CLI.
172+
const regex = /net(?<major>\d)\.(?<minor>\d)/gm;
173+
const match = regex.exec(targetFramework);
174+
if (match) {
175+
const major = parseInt(match.groups?.major || '0', 10);
176+
return major >= 5;
177+
}
178+
166179
return true;
167180
}
168181

169182
// Otherwise it's something before .NET 5 and we're out
170183
return false;
171184
}
172185

186+
// The V3 uses PackageOverrides files from the dotnet SDK to resolve the version
187+
// of packages shipped with the dotnet SDK rather than downloaded from Nuget.
188+
// The logic works for any project using a supported project SDK, see
189+
// https://learn.microsoft.com/en-us/dotnet/core/project-sdk/overview.
190+
function isSupportedByV3GraphGeneration(
191+
targetFramework: string,
192+
projectSdk: string | undefined,
193+
): boolean {
194+
// TargetFramework is required for valid projects.
195+
if (!targetFramework) {
196+
return false;
197+
}
198+
199+
// What's been tested:
200+
// - EOL targets: Windows Phone (wp), Silverlight (sl), .NET Core: netcoreappN.N
201+
// - .NET 5+ netN.N
202+
// - .NET Standard: netstandardN.N
203+
// - .NET Framework: netNN or netNNN
204+
205+
// As long as they use a supported SDK style, they can be scanned.
206+
// These are the SDKs that produce the necessary obj/project.assets.json file
207+
// with the project name and target framework dependencies.
208+
// Uno imports the Microsoft.NET.Sdk behind the scene, so is also supported.
209+
return [
210+
'Microsoft.NET.Sdk',
211+
'MSBuild.Sdk.Extras',
212+
'MSTest.Sdk',
213+
'Uno.Sdk',
214+
].some((sdk) => (projectSdk || '').startsWith(sdk));
215+
}
216+
173217
function extractTargetFrameworksFromFiles(
174218
root: string,
175219
manifestFilePath: string,
@@ -212,6 +256,13 @@ function extractTargetFrameworksFromFiles(
212256
}
213257
}
214258

259+
async function extractProjectSdkFromProjectFile(
260+
manifestFileContents: string,
261+
): Promise<string | undefined> {
262+
const manifestFile: object = await parseXmlFile(manifestFileContents);
263+
return getSdkFromProjectFile(manifestFile);
264+
}
265+
215266
async function extractTargetFrameworksFromProjectFile(
216267
manifestFileContents: string,
217268
): Promise<string[]> {

lib/parsers/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,16 @@ export function getTargetFrameworksFromProjectFile(manifestFile) {
414414
return uniq(targetFrameworksResult);
415415
}
416416

417+
// Extracts the SDK name for SDK-style projects, based on documentation at
418+
// https://learn.microsoft.com/en-us/dotnet/core/project-sdk/overview.
419+
export function getSdkFromProjectFile(manifestFile: any): string | undefined {
420+
const projectSdkAttribute: string | undefined = manifestFile?.Project?.$?.Sdk;
421+
const topLevelSdkElement: string | undefined =
422+
manifestFile?.Project?.Sdk?.[0]?.$?.Name;
423+
424+
return projectSdkAttribute || topLevelSdkElement;
425+
}
426+
417427
function getTargetFrameworks(item: string | any) {
418428
if (typeof item === 'object' && Object.hasOwnProperty.call(item, '_')) {
419429
item = item._;

test/lib/target-frameworks.spec.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
extractTargetFrameworksFromFiles,
3+
extractProjectSdkFromProjectFile,
34
isSupportedByV2GraphGeneration,
5+
isSupportedByV3GraphGeneration,
46
} from '../../lib';
57

68
describe('Target framework tests', () => {
@@ -273,10 +275,178 @@ describe('Target framework tests', () => {
273275
targetFramework: 'net48',
274276
expected: false,
275277
},
278+
// .NET Framework < 5 with dot
279+
{
280+
targetFramework: 'net4.8',
281+
expected: false,
282+
},
276283
])(
277284
'accepts or rejects specific target frameworks for runtime assembly parsing when targetFramework is: $targetFramework.original',
278285
({ targetFramework, expected }) => {
279286
expect(isSupportedByV2GraphGeneration(targetFramework)).toEqual(expected);
280287
},
281288
);
282289
});
290+
291+
describe('SDK project type tests', () => {
292+
it.each([
293+
{
294+
description: 'Project Sdk attribute - Web',
295+
manifest: `
296+
<Project Sdk="Microsoft.NET.Sdk.Web">
297+
</Project>
298+
`,
299+
expectedProjectSdk: 'Microsoft.NET.Sdk.Web',
300+
},
301+
{
302+
description: 'Project Sdk attribute - MSBuild',
303+
manifest: `
304+
<Project Sdk="MSBuild.Sdk.Extras/2.0.54">
305+
</Project>
306+
`,
307+
expectedProjectSdk: 'MSBuild.Sdk.Extras/2.0.54',
308+
},
309+
{
310+
description: 'Top level Sdk element - MSTest',
311+
manifest: `
312+
<Project>
313+
<Sdk Name="MSTest.Sdk/3.8.3" />
314+
</Project>
315+
`,
316+
expectedProjectSdk: 'MSTest.Sdk/3.8.3',
317+
},
318+
{
319+
description:
320+
'Additive Sdk elements (project attribute takes precedence) - Aspire',
321+
manifest: `
322+
<Project Sdk="Microsoft.NET.Sdk">
323+
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
324+
<ItemGroup>
325+
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0"/>
326+
</ItemGroup>
327+
</Project>
328+
`,
329+
expectedProjectSdk: 'Microsoft.NET.Sdk',
330+
},
331+
{
332+
description: 'Custom csproj not using and SDK',
333+
manifest: `
334+
<?xml version="1.0" encoding="utf-8"?>
335+
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
336+
<Import Project="$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\Microsoft.Common.props')" />
337+
<PropertyGroup>
338+
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
339+
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
340+
<ProjectGuid>{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}</ProjectGuid>
341+
<OutputType>Exe</OutputType>
342+
<AppDesignerFolder>Properties</AppDesignerFolder>
343+
<RootNamespace>MyLegacyNetFxApp</RootNamespace>
344+
<AssemblyName>MyLegacyNetFxApp</AssemblyName>
345+
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
346+
<FileAlignment>512</FileAlignment>
347+
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
348+
<Deterministic>true</Deterministic>
349+
</PropertyGroup>
350+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
351+
<PlatformTarget>AnyCPU</PlatformTarget>
352+
<DebugSymbols>true</DebugSymbols>
353+
<DebugType>full</DebugType>
354+
<Optimize>false</Optimize>
355+
<OutputPath>bin\\Debug\\</OutputPath>
356+
<DefineConstants>DEBUG;TRACE</DefineConstants>
357+
<ErrorReport>prompt</ErrorReport>
358+
<WarningLevel>4</WarningLevel>
359+
</PropertyGroup>
360+
<Import Project="$(MSBuildToolsPath)\\Microsoft.CSharp.targets" />
361+
</Project>
362+
`,
363+
expectedProjectSdk: undefined,
364+
},
365+
{
366+
description: 'Unsupported SDK - Godot',
367+
manifest: `
368+
<Project Sdk="Godot.NET.Sdk/4.3.0">
369+
</Project>
370+
`,
371+
expectedProjectSdk: 'Godot.NET.Sdk/4.3.0',
372+
},
373+
{
374+
description: 'Unsupported SDK - Uno',
375+
manifest: `
376+
<Project Sdk="Uno.Sdk/6.0.96">
377+
</Project>
378+
`,
379+
expectedProjectSdk: 'Uno.Sdk/6.0.96',
380+
},
381+
])(
382+
'.Net .csproj is SDK-style project: $description',
383+
async ({ manifest, expectedProjectSdk }) => {
384+
const projectSdk = await extractProjectSdkFromProjectFile(manifest);
385+
expect(projectSdk).toEqual(expectedProjectSdk);
386+
},
387+
);
388+
389+
it.each([
390+
// No target framework, no way to restore the project.
391+
{
392+
targetFramework: '',
393+
projectSdk: 'Microsoft.NET.Sdk',
394+
expected: false,
395+
},
396+
// .NET Core with NET SDK.
397+
{
398+
targetFramework: 'netcoreapp3.1',
399+
projectSdk: 'Microsoft.NET.Sdk',
400+
expected: true,
401+
},
402+
// .NET Standard.
403+
{
404+
targetFramework: 'netstandard1.5',
405+
projectSdk: 'Microsoft.NET.Sdk',
406+
expected: true,
407+
},
408+
// .NET >= 5
409+
{
410+
targetFramework: 'net7.0',
411+
projectSdk: 'Microsoft.NET.Sdk',
412+
expected: true,
413+
},
414+
// .NET Framework 3.5
415+
{
416+
targetFramework: 'net35',
417+
projectSdk: 'Microsoft.NET.Sdk',
418+
expected: true,
419+
},
420+
// .NET Framework 4
421+
{
422+
targetFramework: 'net481',
423+
projectSdk: 'Microsoft.NET.Sdk',
424+
expected: true,
425+
},
426+
// Uno SDK with any supported .NET framework.
427+
{
428+
targetFramework: 'net9.0',
429+
projectSdk: 'Uno.Sdk/6.0.96',
430+
expected: true,
431+
},
432+
// Unsupported Godot SDK, as it generated project.assets.json in a subfolder of .godot and requires mono.
433+
{
434+
targetFramework: 'net9.0',
435+
projectSdk: 'Godot.NET.Sdk/4.3.0',
436+
expected: false,
437+
},
438+
// Missing SDK is unsupported, as it defines how and where msbuild generates project.assets.json.
439+
{
440+
targetFramework: 'net9.0',
441+
projectSdk: undefined,
442+
expected: false,
443+
},
444+
])(
445+
'is v3 graph generation supported for: $targetFramework + $projectSdk',
446+
async ({ targetFramework, projectSdk, expected }) => {
447+
expect(
448+
isSupportedByV3GraphGeneration(targetFramework, projectSdk),
449+
).toEqual(expected);
450+
},
451+
);
452+
});

0 commit comments

Comments
 (0)