diff --git a/.github/workflows/dotnet-sdk.yml b/.github/workflows/dotnet-sdk.yml index cbe7c7c..1d8dfc8 100644 --- a/.github/workflows/dotnet-sdk.yml +++ b/.github/workflows/dotnet-sdk.yml @@ -96,6 +96,11 @@ jobs: shell: pwsh run: scripts/make-changelog.ps1 "${{ env.VERSION }}" "${{ github.sha }}" + - name: Update Analyzer Releases + if: ${{ env.SHOULD_RELEASE == 'True' }} + shell: pwsh + run: scripts/make-analyzer-releases.ps1 + - name: Commit Metadata if: ${{ env.SHOULD_RELEASE == 'True' }} shell: pwsh diff --git a/CLAUDE.md b/CLAUDE.md index 57ee85e..e452d23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,9 +87,14 @@ The SDK automatically detects project types based on naming conventions: Properties set based on detection: `IsPrimaryProject`, `IsCliProject`, `IsAppProject`, `IsTestProject` -### Automatic Project References +### Analyzer-Enforced Requirements -Non-primary projects automatically reference the primary project if it exists. Test projects automatically get `InternalsVisibleTo` access from the projects they test. +The SDK uses Roslyn analyzers to enforce proper project configuration: + +- **KTSU0001 (Error)**: Projects must include required standard packages (SourceLink, Polyfill, System.Memory, System.Threading.Tasks.Extensions). Requirements vary based on project type and target framework. +- **KTSU0002 (Error)**: Projects must expose internals to test projects using `[assembly: InternalsVisibleTo(...)]`. A code fixer is available to automatically add this attribute. + +These properties are passed to analyzers via `CompilerVisibleProperty`: `IsTestProject`, `TestProjectExists`, `TestProjectNamespace`, `TargetFramework`, `TargetFrameworkIdentifier`. ### Metadata File Integration diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 7824e83..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,113 +0,0 @@ - - - - ktsu.$(MSBuildProjectName.Replace(" ", "")) - Library - - - dotnet-sdk.yml - copilot-instructions.md - $([MSBuild]::NormalizePath("$(SolutionDir)\.github")) - $([MSBuild]::NormalizePath("$(GitHubDir)\workflows")) - $([MSBuild]::NormalizePath("$(GitHubWorkflowDir)\$(GitHubWorkflowFileName)")) - $([MSBuild]::NormalizePath("$(GitHubDir)\$(GitHubCopilotInstructionsFileName)")) - - - .gitignore - .gitattributes - .gitconfig - .gitmodules - .mailmap - $([MSBuild]::NormalizePath("$(SolutionDir)\$(GitIgnoreFileName)")) - $([MSBuild]::NormalizePath("$(SolutionDir)\$(GitAttributesFileName)")) - $([MSBuild]::NormalizePath("$(SolutionDir)\$(GitConfigFileName)")) - $([MSBuild]::NormalizePath("$(SolutionDir)\$(GitModulesFileName)")) - $([MSBuild]::NormalizePath("$(SolutionDir)\$(GitMailMapFileName)")) - - - .editorconfig - .runsettings - $([MSBuild]::NormalizePath("$(SolutionDir)\$(EditorConfigFileName)")) - $([MSBuild]::NormalizePath("$(SolutionDir)\$(RunSettingsFileName)")) - - - $(SolutionName) - $(PrimaryProjectName).csproj - $([MSBuild]::NormalizePath("$(SolutionDir)\$(PrimaryProjectName)\$(PrimaryProjectFileName)")) - false - true - - - CONTRIBUTORS.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(ContributorsFileName)")) - $([System.IO.File]::ReadAllText($(ContributorsFilePath)).Trim()) - - CHANGELOG.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(ChangelogFileName)")) - $([System.IO.File]::ReadAllText($(ChangelogFilePath)).Trim()) - - README.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(ReadmeFileName)")) - $([System.IO.File]::ReadAllText($(ReadmeFilePath)).Trim()) - - LICENSE.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(LicenseFileName)")) - $([System.IO.File]::ReadAllText($(LicenseFilePath)).Trim()) - - AUTHORS.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(AuthorsFileName)")) - $([System.IO.File]::ReadAllText($(AuthorsFilePath)).Trim()) - - AUTHORS.url - $([MSBuild]::NormalizePath("$(SolutionDir)\$(AuthorsUrlFileName)")) - $([System.IO.File]::ReadAllText($(AuthorsUrlFilePath)).Trim()) - - PROJECT.url - $([MSBuild]::NormalizePath("$(SolutionDir)\$(ProjectUrlFileName)")) - $([System.IO.File]::ReadAllText($(ProjectUrlFilePath)).Trim()) - - COPYRIGHT.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(CopyrightFileName)")) - $([System.IO.File]::ReadAllText($(CopyrightFilePath)).Trim()) - - VERSION.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(VersionFileName)")) - $([System.IO.File]::ReadAllText($(VersionFilePath)).Trim()) - - DESCRIPTION.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(DescriptionFileName)")) - $([System.IO.File]::ReadAllText($(DescriptionFilePath)).Trim()) - - TAGS.md - $([MSBuild]::NormalizePath("$(SolutionDir)\$(TagsFileName)")) - $([System.IO.File]::ReadAllText($(TagsFilePath)).Trim()) - - icon.png - $([MSBuild]::NormalizePath("$(SolutionDir)\$(IconFileName)")) - - - $(AssemblyName) - $(Authors) - $(AssemblyName) - - MSBuildSdk - $(AssemblyName) - $(Version) - $(IconFileName) - $(ReadmeFileName) - $(LicenseFileName) - $(Changelog) - $(Description) - $(Tags) - $(ProjectUrl) - true - true - true - snupkg - true - - - false - true - - diff --git a/README.md b/README.md index 5332b31..c2f2ced 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,8 @@ For a GUI application: ### 🔧 **Development Workflow** -- **Automatic Project References**: Smart cross-project referencing based on project types -- **Internals Visibility**: Automatic InternalsVisibleTo configuration for test projects +- **Analyzer-Enforced Requirements**: Roslyn analyzers (KTSU0001/KTSU0002) ensure proper package dependencies and internals visibility with helpful diagnostics and code fixers +- **Internals Visibility**: Code fixer to easily add InternalsVisibleTo attributes for test projects - **GitHub Integration**: Built-in support for GitHub workflows and CI/CD - **Cross-Platform Support**: Compatible with Windows, macOS, and Linux - **Documentation Generation**: Automated XML documentation file generation @@ -229,14 +229,21 @@ This enables the SDK to work with any nested project structure without configura ## Advanced Configuration Features -### Automatic Project References +### Analyzer-Enforced Requirements -Projects automatically reference the primary project and expose internals to test projects. Cross-references are intelligently configured based on project types and naming conventions. +The SDK includes Roslyn analyzers that enforce proper project configuration with helpful diagnostics and code fixers: -For example: +**KTSU0001 (Error)**: Projects must include required standard packages +- Enforces SourceLink packages (GitHub, Azure Repos) +- Enforces Polyfill package for non-test projects +- Enforces compatibility packages (System.Memory, System.Threading.Tasks.Extensions) based on target framework +- Diagnostic message includes package name and version number -- Non-primary projects automatically get `` to the primary project -- Primary project automatically exposes internals to test projects via `` +**KTSU0002 (Error)**: Projects must expose internals to test projects +- Code fixer automatically adds `[assembly: InternalsVisibleTo(...)]` attribute +- Use Ctrl+. (Quick Actions) to apply the fix + +These analyzers ensure consistent project structure while giving you explicit control over dependencies. ### Available Properties diff --git a/Sdk.Analyzers/AddInternalsVisibleToAttributeCodeFixProvider.cs b/Sdk.Analyzers/AddInternalsVisibleToAttributeCodeFixProvider.cs new file mode 100644 index 0000000..c447cf1 --- /dev/null +++ b/Sdk.Analyzers/AddInternalsVisibleToAttributeCodeFixProvider.cs @@ -0,0 +1,114 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Sdk.Analyzers; + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Code fix provider that adds InternalsVisibleToAttribute to expose internals to test projects +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddInternalsVisibleToAttributeCodeFixProvider))] +[Shared] +public class AddInternalsVisibleToAttributeCodeFixProvider : CodeFixProvider +{ + + /// + public override ImmutableArray FixableDiagnosticIds => [MissingInternalsVisibleToAttributeAnalyzer.DiagnosticId]; + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + Diagnostic diagnostic = context.Diagnostics.First(); + + context.RegisterCodeFix( + CodeAction.Create( + title: "Add [assembly: InternalsVisibleTo(...)]", + createChangedDocument: ct => AddInternalsVisibleToAttributeAsync(context.Document, diagnostic, ct), + equivalenceKey: nameof(AddInternalsVisibleToAttributeCodeFixProvider)), + diagnostic); + } + + private static async Task AddInternalsVisibleToAttributeAsync( + Document document, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is not CompilationUnitSyntax compilationUnit) + { + return document; + } + + // Get test project namespace from analyzer config options + + AnalyzerConfigOptions options = document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions; + if (!options.TryGetValue("build_property.TestProjectNamespace", out string? testNamespace) || string.IsNullOrWhiteSpace(testNamespace)) + { + return document; + } + + // Check if using directive already exists + + bool hasUsing = compilationUnit.Usings.Any(u => + u.Name?.ToString() == "System.Runtime.CompilerServices"); + + // Create the using directive if needed + + SyntaxList newUsings = compilationUnit.Usings; + if (!hasUsing) + { + UsingDirectiveSyntax usingDirective = SyntaxFactory.UsingDirective( + SyntaxFactory.ParseName("System.Runtime.CompilerServices")) + .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed); + newUsings = newUsings.Add(usingDirective); + } + + // Create the InternalsVisibleTo attribute + + AttributeArgumentSyntax attributeArgument = SyntaxFactory.AttributeArgument( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(testNamespace))); + + AttributeSyntax attribute = SyntaxFactory.Attribute( + SyntaxFactory.ParseName("System.Runtime.CompilerServices.InternalsVisibleTo"), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList(attributeArgument))); + + AttributeListSyntax attributeList = SyntaxFactory.AttributeList( + SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.AssemblyKeyword)), + SyntaxFactory.SingletonSeparatedList(attribute)) + .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed); + + // Add the attribute to the compilation unit + + CompilationUnitSyntax newCompilationUnit = compilationUnit + .WithUsings(newUsings) + .AddAttributeLists(attributeList) + .WithLeadingTrivia(compilationUnit.GetLeadingTrivia()) + .WithTrailingTrivia(compilationUnit.GetTrailingTrivia()); + + return document.WithSyntaxRoot(newCompilationUnit); + } +} diff --git a/Sdk.Analyzers/AnalyzerReleases.Shipped.md b/Sdk.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..509f7d0 --- /dev/null +++ b/Sdk.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,12 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release {version} + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +KTSU0001 | ktsu.Sdk | Error | Missing required package reference +KTSU0002 | ktsu.Sdk | Error | Missing InternalsVisibleTo attribute for test project + diff --git a/Sdk.Analyzers/AnalyzerReleases.Unshipped.md b/Sdk.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..e3690dd --- /dev/null +++ b/Sdk.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,7 @@ +; Unshipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- diff --git a/Sdk.Analyzers/KtsuAnalyzerBase.cs b/Sdk.Analyzers/KtsuAnalyzerBase.cs new file mode 100644 index 0000000..87404cb --- /dev/null +++ b/Sdk.Analyzers/KtsuAnalyzerBase.cs @@ -0,0 +1,19 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Sdk.Analyzers; + +using Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Base class for all ktsu.Sdk analyzers +/// +public abstract class KtsuAnalyzerBase : DiagnosticAnalyzer +{ + + /// + /// Category for ktsu.Sdk analyzers + /// + protected const string Category = "ktsu.Sdk"; +} diff --git a/Sdk.Analyzers/MissingInternalsVisibleToAttributeAnalyzer.cs b/Sdk.Analyzers/MissingInternalsVisibleToAttributeAnalyzer.cs new file mode 100644 index 0000000..4ae19bb --- /dev/null +++ b/Sdk.Analyzers/MissingInternalsVisibleToAttributeAnalyzer.cs @@ -0,0 +1,111 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Sdk.Analyzers; + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Analyzer that suggests exposing internals to test projects via InternalsVisibleToAttribute +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class MissingInternalsVisibleToAttributeAnalyzer : KtsuAnalyzerBase +{ + + /// + /// Diagnostic ID for this analyzer + /// + public const string DiagnosticId = "KTSU0002"; + + private static readonly LocalizableString Title = "Missing InternalsVisibleTo attribute for test project"; + private static readonly LocalizableString MessageFormat = "Consider exposing internals to test project '{0}'. Add '[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(\"{0}\")]' to a .cs file."; + private static readonly LocalizableString Description = "Projects should expose their internal members to test projects using the InternalsVisibleToAttribute for comprehensive testing."; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: Description, + customTags: "CompilationEnd"); + + /// + public override ImmutableArray SupportedDiagnostics => [Rule]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationAction(AnalyzeCompilation); + } + + private static void AnalyzeCompilation(CompilationAnalysisContext context) + { + AnalyzerConfigOptions options = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions; + + // Check if this is a non-test project + + if (!options.TryGetValue("build_property.IsTestProject", out string? isTest) || isTest != "false") + { + return; + } + + // Check if a test project exists + + if (!options.TryGetValue("build_property.TestProjectExists", out string? exists) || exists != "true") + { + return; + } + + // Get test project namespace + + if (!options.TryGetValue("build_property.TestProjectNamespace", out string? testNamespace) || string.IsNullOrWhiteSpace(testNamespace)) + { + return; + } + + // Check if InternalsVisibleToAttribute already exists for the test project + + IEnumerable internalsVisibleToAttributes = context.Compilation.Assembly + .GetAttributes() + .Where(attr => attr.AttributeClass?.Name == "InternalsVisibleToAttribute" && + attr.AttributeClass.ContainingNamespace?.ToDisplayString() == "System.Runtime.CompilerServices"); + + bool hasTestReference = internalsVisibleToAttributes.Any(attr => + { + if (attr.ConstructorArguments.Length > 0) + { + TypedConstant firstArg = attr.ConstructorArguments[0]; + if (firstArg.Kind == TypedConstantKind.Primitive && firstArg.Value is string assemblyName) + { + // Handle both non-strong-named ("MyTest") and strong-named ("MyTest, PublicKey=...") assemblies + return assemblyName == testNamespace || + assemblyName.StartsWith(testNamespace + ",", System.StringComparison.Ordinal); + } + } + return false; + }); + + if (!hasTestReference) + { + // Report diagnostic at the first syntax tree location (project-level diagnostic) + + Location location = context.Compilation.SyntaxTrees.FirstOrDefault()?.GetRoot().GetLocation() ?? Location.None; + + Diagnostic diagnostic = Diagnostic.Create( + Rule, + location, + testNamespace); + + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/Sdk.Analyzers/MissingStandardPackagesAnalyzer.cs b/Sdk.Analyzers/MissingStandardPackagesAnalyzer.cs new file mode 100644 index 0000000..d444273 --- /dev/null +++ b/Sdk.Analyzers/MissingStandardPackagesAnalyzer.cs @@ -0,0 +1,149 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Sdk.Analyzers; + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Analyzer that enforces required standard package references +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class MissingStandardPackagesAnalyzer : KtsuAnalyzerBase +{ + + /// + /// Diagnostic ID for this analyzer + /// + public const string DiagnosticId = "KTSU0001"; + + private static readonly LocalizableString Title = "Missing required package reference"; + private static readonly LocalizableString MessageFormat = "Project must reference package '{0}'. Add '' to your .csproj file."; + private static readonly LocalizableString Description = "Projects should include required standard packages for SourceLink, polyfills, and framework compatibility."; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: Description, + customTags: "CompilationEnd"); + + /// + public override ImmutableArray SupportedDiagnostics => [Rule]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationAction(AnalyzeCompilation); + } + + private static void AnalyzeCompilation(CompilationAnalysisContext context) + { + AnalyzerConfigOptions options = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions; + + // Get project properties + + options.TryGetValue("build_property.IsTestProject", out string? isTestProject); + options.TryGetValue("build_property.TargetFramework", out string? targetFramework); + options.TryGetValue("build_property.TargetFrameworkIdentifier", out string? targetFrameworkIdentifier); + + // Get package reference properties (passed from MSBuild) + + options.TryGetValue("build_property.HasSourceLinkGitHub", out string? hasSourceLinkGitHub); + options.TryGetValue("build_property.HasSourceLinkAzureRepos", out string? hasSourceLinkAzureRepos); + options.TryGetValue("build_property.HasPolyfill", out string? hasPolyfill); + options.TryGetValue("build_property.HasSystemMemory", out string? hasSystemMemory); + options.TryGetValue("build_property.HasSystemThreadingTasksExtensions", out string? hasSystemThreadingTasksExtensions); + + // Check for Microsoft.SourceLink.GitHub (build-time-only package) + + CheckPackageProperty(context, "Microsoft.SourceLink.GitHub", hasSourceLinkGitHub); + + // Check for Microsoft.SourceLink.AzureRepos.Git (build-time-only package) + + CheckPackageProperty(context, "Microsoft.SourceLink.AzureRepos.Git", hasSourceLinkAzureRepos); + + // Check for Polyfill (build-time-only package, non-test projects only) + + if (isTestProject != "true") + { + CheckPackageProperty(context, "Polyfill", hasPolyfill); + } + + // Check for System.Memory (conditional on target framework) + + if (RequiresSystemMemory(targetFramework, targetFrameworkIdentifier)) + { + CheckPackageProperty(context, "System.Memory", hasSystemMemory); + } + + // Check for System.Threading.Tasks.Extensions (conditional on target framework) + + if (RequiresTaskExtensions(targetFramework, targetFrameworkIdentifier)) + { + CheckPackageProperty(context, "System.Threading.Tasks.Extensions", hasSystemThreadingTasksExtensions); + } + } + + private static void CheckPackageProperty(CompilationAnalysisContext context, string packageName, string? hasPackageProperty) + { + // Check if package reference exists via MSBuild property + // Build-time-only packages (with PrivateAssets=all) don't appear in compilation references, + // so we need to check MSBuild properties passed via CompilerVisibleProperty + + bool hasPackage = hasPackageProperty == "true"; + + if (!hasPackage) + { + Location location = context.Compilation.SyntaxTrees.FirstOrDefault()?.GetRoot().GetLocation() ?? Location.None; + + Diagnostic diagnostic = Diagnostic.Create( + Rule, + location, + packageName); + + context.ReportDiagnostic(diagnostic); + } + } + + private static bool RequiresSystemMemory(string? targetFramework, string? targetFrameworkIdentifier) + { + if (string.IsNullOrEmpty(targetFramework)) + { + return false; + } + + // Condition: $(TargetFrameworkIdentifier) == '.NETStandard' or + // $(TargetFrameworkIdentifier) == '.NETFramework' or + // $(TargetFramework.StartsWith('netcoreapp2')) + + return targetFrameworkIdentifier == ".NETStandard" || + targetFrameworkIdentifier == ".NETFramework" || + targetFramework.StartsWith("netcoreapp2", System.StringComparison.Ordinal); + } + + private static bool RequiresTaskExtensions(string? targetFramework, string? targetFrameworkIdentifier) + { + if (string.IsNullOrEmpty(targetFramework)) + { + return false; + } + + // Condition: $(TargetFramework) == 'netstandard2.0' or + // $(TargetFramework) == 'netcoreapp2.0' or + // $(TargetFrameworkIdentifier) == '.NETFramework' + + return targetFramework == "netstandard2.0" || + targetFramework == "netcoreapp2.0" || + targetFrameworkIdentifier == ".NETFramework"; + } +} diff --git a/Sdk.Analyzers/Sdk.Analyzers.csproj b/Sdk.Analyzers/Sdk.Analyzers.csproj new file mode 100644 index 0000000..1994c54 --- /dev/null +++ b/Sdk.Analyzers/Sdk.Analyzers.csproj @@ -0,0 +1,43 @@ + + + + + + + + + netstandard2.0 + latest + enable + true + true + + + $(NoWarn);RS1038 + + + Analyzer + false + true + true + + + false + false + + + + + + + + + + + + + + + + + diff --git a/Sdk.App/Sdk.App.csproj b/Sdk.App/Sdk.App.csproj index 3463f22..f490d84 100644 --- a/Sdk.App/Sdk.App.csproj +++ b/Sdk.App/Sdk.App.csproj @@ -1,5 +1,16 @@ + + + + + net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.0;netstandard2.1 + + MSBuildSdk + + + + diff --git a/Sdk.Common.MetadataFiles.props b/Sdk.Common.MetadataFiles.props new file mode 100644 index 0000000..e76df61 --- /dev/null +++ b/Sdk.Common.MetadataFiles.props @@ -0,0 +1,62 @@ + + + + + AUTHORS.md + $([MSBuild]::NormalizePath("$(SolutionDir)\$(AuthorsFileName)")) + $([System.IO.File]::ReadAllText($(AuthorsFilePath)).Trim()) + + + VERSION.md + $([MSBuild]::NormalizePath("$(SolutionDir)\$(VersionFileName)")) + $([System.IO.File]::ReadAllText($(VersionFilePath)).Trim()) + + + DESCRIPTION.md + $([MSBuild]::NormalizePath("$(SolutionDir)\$(DescriptionFileName)")) + $([System.IO.File]::ReadAllText($(DescriptionFilePath)).Trim()) + + + TAGS.md + $([MSBuild]::NormalizePath("$(SolutionDir)\$(TagsFileName)")) + $([System.IO.File]::ReadAllText($(TagsFilePath)).Trim()) + + + LICENSE.md + $([MSBuild]::NormalizePath("$(SolutionDir)\$(LicenseFileName)")) + $([System.IO.File]::ReadAllText($(LicenseFilePath)).Trim()) + + + CHANGELOG.md + $([MSBuild]::NormalizePath("$(SolutionDir)\$(ChangelogFileName)")) + $([System.IO.File]::ReadAllText($(ChangelogFilePath)).Trim()) + + + README.md + $([MSBuild]::NormalizePath("$(SolutionDir)\$(ReadmeFileName)")) + $([System.IO.File]::ReadAllText($(ReadmeFilePath)).Trim()) + + + icon.png + $([MSBuild]::NormalizePath("$(SolutionDir)\$(IconFileName)")) + + + AUTHORS.url + $([MSBuild]::NormalizePath("$(SolutionDir)\$(AuthorsUrlFileName)")) + $([System.IO.File]::ReadAllText($(AuthorsUrlFilePath)).Trim()) + + + PROJECT.url + $([MSBuild]::NormalizePath("$(SolutionDir)\$(ProjectUrlFileName)")) + $([System.IO.File]::ReadAllText($(ProjectUrlFilePath)).Trim()) + + + COPYRIGHT.md + $([MSBuild]::NormalizePath("$(SolutionDir)\$(CopyrightFileName)")) + $([System.IO.File]::ReadAllText($(CopyrightFilePath)).Trim()) + + diff --git a/Directory.Build.targets b/Sdk.Common.PackageContent.targets similarity index 88% rename from Directory.Build.targets rename to Sdk.Common.PackageContent.targets index 775965d..417ddef 100644 --- a/Directory.Build.targets +++ b/Sdk.Common.PackageContent.targets @@ -1,10 +1,9 @@ + - - - - - diff --git a/Sdk.Common.PackageProperties.props b/Sdk.Common.PackageProperties.props new file mode 100644 index 0000000..9f919ae --- /dev/null +++ b/Sdk.Common.PackageProperties.props @@ -0,0 +1,38 @@ + + + + + ktsu.$(MSBuildProjectName.Replace(" ", "")) + Library + + + $(AssemblyName) + $(Authors) + $(AssemblyName) + $(AssemblyName) + $(Version) + + + $(IconFileName) + $(ReadmeFileName) + $(LicenseFileName) + $(Changelog) + $(Description) + $(Tags) + $(ProjectUrl) + + + true + true + true + snupkg + true + + + false + true + + diff --git a/Sdk.Common.SdkContent.targets b/Sdk.Common.SdkContent.targets new file mode 100644 index 0000000..99de19e --- /dev/null +++ b/Sdk.Common.SdkContent.targets @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Sdk.Common.SolutionDiscovery.props b/Sdk.Common.SolutionDiscovery.props new file mode 100644 index 0000000..08fbe1d --- /dev/null +++ b/Sdk.Common.SolutionDiscovery.props @@ -0,0 +1,30 @@ + + + + + <_SolutionFileExists_Level1 Condition="'$(SolutionDir)' == ''">$([System.IO.Directory]::GetFiles('$(MSBuildProjectDirectory)', '*.sln').Length) + $(MSBuildProjectDirectory) + + + <_SolutionFileExists_Level2 Condition="'$(SolutionDir)' == '' And Exists('$(MSBuildProjectDirectory)\..')">$([System.IO.Directory]::GetFiles('$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..'))','*.sln').Length) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..')) + + + <_SolutionFileExists_Level3 Condition="'$(SolutionDir)' == '' And Exists('$(MSBuildProjectDirectory)\..\..')">$([System.IO.Directory]::GetFiles('$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..'))','*.sln').Length) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..')) + + + <_SolutionFileExists_Level4 Condition="'$(SolutionDir)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\..')">$([System.IO.Directory]::GetFiles('$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\..'))','*.sln').Length) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\..')) + + + <_SolutionFileExists_Level5 Condition="'$(SolutionDir)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\..\..')">$([System.IO.Directory]::GetFiles('$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\..\..\'))','*.sln').Length) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\..\..')) + + + $(MSBuildProjectDirectory) + + diff --git a/Sdk.ConsoleApp/Sdk.ConsoleApp.csproj b/Sdk.ConsoleApp/Sdk.ConsoleApp.csproj index 3463f22..f490d84 100644 --- a/Sdk.ConsoleApp/Sdk.ConsoleApp.csproj +++ b/Sdk.ConsoleApp/Sdk.ConsoleApp.csproj @@ -1,5 +1,16 @@ + + + + + net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.0;netstandard2.1 + + MSBuildSdk + + + + diff --git a/Sdk.sln b/Sdk.sln index cded009..baf4c7d 100644 --- a/Sdk.sln +++ b/Sdk.sln @@ -9,24 +9,66 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sdk.App", "Sdk.App\Sdk.App. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sdk.ConsoleApp", "Sdk.ConsoleApp\Sdk.ConsoleApp.csproj", "{E33FCF20-FACE-A5BF-E0B7-399C6465B204}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sdk.Analyzers", "Sdk.Analyzers\Sdk.Analyzers.csproj", "{E7C6FABE-42DE-4DB6-8CDA-5527789D0538}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Debug|x64.Build.0 = Debug|Any CPU + {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Debug|x86.Build.0 = Debug|Any CPU {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Release|Any CPU.ActiveCfg = Release|Any CPU {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Release|Any CPU.Build.0 = Release|Any CPU + {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Release|x64.ActiveCfg = Release|Any CPU + {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Release|x64.Build.0 = Release|Any CPU + {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Release|x86.ActiveCfg = Release|Any CPU + {49EC93AA-B8C7-4C2F-8E38-D9A7F01383CA}.Release|x86.Build.0 = Release|Any CPU {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Debug|x64.Build.0 = Debug|Any CPU + {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Debug|x86.Build.0 = Debug|Any CPU {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Release|Any CPU.Build.0 = Release|Any CPU + {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Release|x64.ActiveCfg = Release|Any CPU + {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Release|x64.Build.0 = Release|Any CPU + {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Release|x86.ActiveCfg = Release|Any CPU + {D0BA1D34-ED6C-AEC5-4ABA-D13185D9E53E}.Release|x86.Build.0 = Release|Any CPU {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Debug|x64.ActiveCfg = Debug|Any CPU + {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Debug|x64.Build.0 = Debug|Any CPU + {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Debug|x86.ActiveCfg = Debug|Any CPU + {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Debug|x86.Build.0 = Debug|Any CPU {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Release|Any CPU.ActiveCfg = Release|Any CPU {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Release|Any CPU.Build.0 = Release|Any CPU + {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Release|x64.ActiveCfg = Release|Any CPU + {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Release|x64.Build.0 = Release|Any CPU + {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Release|x86.ActiveCfg = Release|Any CPU + {E33FCF20-FACE-A5BF-E0B7-399C6465B204}.Release|x86.Build.0 = Release|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Debug|x64.Build.0 = Debug|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Debug|x86.Build.0 = Debug|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Release|Any CPU.Build.0 = Release|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Release|x64.ActiveCfg = Release|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Release|x64.Build.0 = Release|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Release|x86.ActiveCfg = Release|Any CPU + {E7C6FABE-42DE-4DB6-8CDA-5527789D0538}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Sdk/Sdk.csproj b/Sdk/Sdk.csproj index b2874e9..9119096 100644 --- a/Sdk/Sdk.csproj +++ b/Sdk/Sdk.csproj @@ -1,5 +1,16 @@  + + + + + net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.0;netstandard2.1 + + MSBuildSdk + + + + diff --git a/Sdk/Sdk.targets b/Sdk/Sdk.targets index 8f8bd3b..cccabf8 100644 --- a/Sdk/Sdk.targets +++ b/Sdk/Sdk.targets @@ -25,45 +25,55 @@ - - - - + + + + <_SourceLinkGitHub Include="@(PackageReference)" Condition="'%(Identity)' == 'Microsoft.SourceLink.GitHub'" /> + <_SourceLinkAzureRepos Include="@(PackageReference)" Condition="'%(Identity)' == 'Microsoft.SourceLink.AzureRepos.Git'" /> + <_Polyfill Include="@(PackageReference)" Condition="'%(Identity)' == 'Polyfill'" /> + <_SystemMemory Include="@(PackageReference)" Condition="'%(Identity)' == 'System.Memory'" /> + <_SystemThreadingTasksExtensions Include="@(PackageReference)" Condition="'%(Identity)' == 'System.Threading.Tasks.Extensions'" /> + + + true + false - - - - + true + false - - + true + false - - all - + true + false - - all - + true + false + + - + + + + + + + + + + + + + + + + + all - runtime; build; native; contentfiles; analyzers - - - - + true true diff --git a/scripts/commit-metadata.ps1 b/scripts/commit-metadata.ps1 index 6de2491..25538df 100644 --- a/scripts/commit-metadata.ps1 +++ b/scripts/commit-metadata.ps1 @@ -1,6 +1,14 @@ git config --global user.name "Github Actions" git config --global user.email "actions@users.noreply.github.com" + +# Add standard metadata files git add VERSION.md LICENSE.md AUTHORS.md COPYRIGHT.md CHANGELOG.md PROJECT_URL.url AUTHORS.url + +# Add analyzer releases file if it exists +if (Test-Path "Sdk.Analyzers/AnalyzerReleases.Shipped.md") { + git add Sdk.Analyzers/AnalyzerReleases.Shipped.md +} + git commit -m "[bot][skip ci] Update Metadata" git push diff --git a/scripts/make-analyzer-releases.ps1 b/scripts/make-analyzer-releases.ps1 new file mode 100644 index 0000000..bac1b51 --- /dev/null +++ b/scripts/make-analyzer-releases.ps1 @@ -0,0 +1,42 @@ +Set-PSDebug -Trace 1 + +# Read the version from VERSION.md +if (Test-Path VERSION.md) { + $VERSION = (Get-Content VERSION.md -Raw).Trim() + Write-Host "VERSION: $VERSION" +} else { + Write-Host "VERSION.md not found, skipping analyzer releases update" + exit 0 +} + +# Update AnalyzerReleases.Shipped.md if it exists and contains the placeholder +$SHIPPED_FILE = "Sdk.Analyzers/AnalyzerReleases.Shipped.md" +if (Test-Path $SHIPPED_FILE) { + $CONTENT = Get-Content $SHIPPED_FILE -Raw + if ($CONTENT -match '\{version\}') { + Write-Host "Updating $SHIPPED_FILE with version $VERSION" + $UPDATED_CONTENT = $CONTENT -replace '\{version\}', $VERSION + $UPDATED_CONTENT | Out-File -FilePath $SHIPPED_FILE -Encoding utf8 -NoNewline + Write-Host "Updated $SHIPPED_FILE" + } else { + Write-Host "No {version} placeholder found in $SHIPPED_FILE, skipping update" + } +} else { + Write-Host "$SHIPPED_FILE not found, skipping analyzer releases update" +} + +# Update Sdk.targets files to replace {version} placeholder with actual SDK version +Get-ChildItem -Recurse -Filter "Sdk.targets" | ForEach-Object { + $TARGET_FILE = $_.FullName + $CONTENT = Get-Content $TARGET_FILE -Raw + if ($CONTENT -match '\{version\}') { + Write-Host "Updating $TARGET_FILE with version $VERSION" + $UPDATED_CONTENT = $CONTENT -replace '\{version\}', $VERSION + $UPDATED_CONTENT | Out-File -FilePath $TARGET_FILE -Encoding utf8 -NoNewline + Write-Host "Updated $TARGET_FILE" + } else { + Write-Host "No {version} placeholder found in $TARGET_FILE, skipping update" + } +} + +$global:LASTEXITCODE = 0