Skip to content

Latest commit

 

History

History
274 lines (206 loc) · 16 KB

File metadata and controls

274 lines (206 loc) · 16 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Overview

This is an MSBuild SDK package (ktsu.Sdk) that provides standardized configuration, metadata management, and build workflows for .NET projects. The SDK automatically discovers solution structures, generates namespaces from directory paths, and manages project metadata through markdown files.

Build Commands

Building the Solution

dotnet build --configuration Release --verbosity normal --no-incremental

Testing

dotnet test -m:1 --configuration Release --verbosity normal --no-build

Packaging

dotnet pack --configuration Release --output ./staging

Publishing Applications

dotnet publish <project>.csproj --no-build --configuration Release --framework net10.0 --output ./output/<project>

Version Management

Version management is handled through PowerShell scripts in the scripts/ directory using the PSBuild module:

  • make-version.ps1: Calculates semantic version from git history
  • make-license.ps1: Generates LICENSE.md from template
  • make-changelog.ps1: Generates CHANGELOG.md from git commits
  • commit-metadata.ps1: Commits metadata changes with proper attribution

Version calculation rules:

  • [major] tag in commit: major version increment (breaking changes)
  • [minor] tag or public API changes: minor version increment
  • [patch] tag or code changes: patch version increment
  • [pre] tag or minimal changes: prerelease increment

The PSBuild module automatically detects public API changes by analyzing diffs for modifications to public classes, interfaces, methods, properties, etc.

Project Structure

The SDK consists of multiple sub-SDKs:

  • Sdk/: Core SDK with MSBuild props and targets (all project types)

    • Sdk.props: Hierarchical solution discovery, metadata file loading, namespace generation, package configuration
    • Sdk.targets: Project type detection, automatic references, package inclusion logic
  • Sdk.ConsoleApp/: Console application SDK

    • Sets OutputType=Exe and TargetFramework=net10.0
  • Sdk.App/: GUI application SDK (ImGui/Windows apps)

    • Sets OutputType=WinExe on Windows, Exe on other platforms
    • Sets TargetFramework=net10.0
    • Configures runtime identifiers for cross-platform GUI support
  • Sdk.Tool/: .NET tool SDK (dotnet tool install)

    • Sets PackAsTool=true, OutputType=Exe, TargetFramework=net10.0
    • Clears RuntimeIdentifiers: under PackAsTool the .NET 10 SDK turns each RID in the inherited desktop list into a separate RID-specific tool package, so one dotnet pack emits seven packages racing over a single intermediate output directory. Tools here are framework-dependent and RID-agnostic — consumers need the .NET 10 runtime.
    • Derives ToolCommandName from the lowercased solution name (stripping a trailing .tool/.cli), because the default would be AssemblyName, which the core SDK forces to the fully-qualified namespace (ktsu.KtsuBuild.Tool). Derived in Sdk.props, not Sdk.targets, so the value is set before Microsoft.NET.Sdk defaults it.
    • Disables package validation and IncludeSource, which are library-oriented
    • Sets IsPublishable=true in Sdk.props. PackAsTool builds the tools/ payload from a publish, which is gated on IsPublishable; without it the package contains only DotnetToolSettings.xml and none of the assemblies it points at — it installs, then fails at run time. The core SDK's Sdk.targets flip (false in props, true for OutputType=Exe in targets) is too late, for the same import-ordering reason as ToolCommandName. Tool projects stay out of CI's RID zip publishing by project selection (KtsuBuild scans the csproj text), not by this property.
    • Errors (KTSU1001) if TargetFrameworks is set: a tool package cannot multi-target
  • Sdk.Windows/, Sdk.Linux/, Sdk.macOS/: Desktop per-OS app SDKs

    • RID-based presets on the base net10.0 runtime (no extra prerequisites)
    • Narrow RuntimeIdentifiers to the target OS and default RuntimeIdentifier
    • Windows uses OutputType=WinExe; Linux/macOS use Exe
  • Sdk.iOS/, Sdk.Android/: Mobile app SDKs (TFM + workload based)

    • Set TargetFramework=net10.0-ios / net10.0-android plus SupportedOSPlatformVersion
    • Consuming projects require the ios/android workloads (dotnet workload install android ios maui); iOS additionally needs a macOS host
    • The SDK packages themselves carry no workload dependency and pack on any host

Key SDK Features

Hierarchical Solution Discovery

The SDK searches up to 5 directory levels from the project directory to find solution files. This enables nested project structures without manual configuration.

Path-Based Namespace Generation

Namespaces are automatically generated from directory structure:

MySolution/src/Core/Utils/MyProject.csproj
→ ProjectNamespace: src.Core.Utils.MyProject
→ RootNamespace: {AuthorsNamespace}.src.Core.Utils.MyProject

The SDK intelligently handles cases where the directory name matches the project name to avoid duplication.

Project Type Detection

The SDK automatically detects project types based on naming conventions:

  • Primary Project: {SolutionName} or {SolutionName}.Core
  • Console Projects: {SolutionName}.CLI, {SolutionName}.Cli, {SolutionName}Cli, {SolutionName}CLI, {SolutionName}.ConsoleApp, {SolutionName}.Console
  • GUI Projects: {SolutionName}.App, {SolutionName}App, {SolutionName}.WinApp, {SolutionName}WinApp, {SolutionName}.ImGuiApp, {SolutionName}ImGuiApp
  • iOS Projects: {SolutionName}.iOS, {SolutionName}iOS, {SolutionName}.Ios
  • Android Projects: {SolutionName}.Android, {SolutionName}Android, {SolutionName}.Droid
  • Windows Projects: {SolutionName}.Windows, {SolutionName}Windows, {SolutionName}.Win
  • Linux Projects: {SolutionName}.Linux, {SolutionName}Linux
  • macOS Projects: {SolutionName}.macOS, {SolutionName}.MacOS, {SolutionName}.Mac
  • Tool Projects: {SolutionName}.Tool, {SolutionName}Tool — deliberately not .CLI, so no existing console project silently starts publishing itself as a tool package
  • Test Projects: {SolutionName}.Test, {SolutionName}.Tests, {SolutionName}Test, {SolutionName}Tests

Properties set based on detection: IsPrimaryProject, IsCliProject, IsAppProject, IsToolProject, IsIosProject, IsAndroidProject, IsWindowsProject, IsLinuxProject, IsMacProject, IsTestProject

Analyzer-Enforced Requirements

The SDK automatically includes the ktsu.Sdk.Analyzers package (with version synchronization via {version} placeholder) to enforce proper project configuration:

  • KTSU0001 (Error): Projects must include required standard packages (Polyfill, System.Memory, System.Threading.Tasks.Extensions). Requirements vary based on project type and target framework. SourceLink is intentionally not required: the .NET 8+ SDK bundles SourceLink and enables it implicitly, and an explicit Microsoft.SourceLink.* PackageReference re-enables the noisy "Source control information is not available" warning for any build without a usable remote. Consumers should not reference SourceLink packages directly.
  • KTSU0002 (Error): Projects must expose internals to test projects using [assembly: InternalsVisibleTo(...)]. A code fixer is available to automatically add this attribute.
  • KTSU0003 (Error): Use Ensure.NotNull() over ArgumentNullException.ThrowIfNull() for better framework compatibility. A code fixer is available to automatically replace the invocation.
  • KTSU0004 (Error): Use Ensure.NotNull() instead of manual null checks with ArgumentNullException. Detects patterns like if (x == null) throw new ArgumentNullException(...), if (x is null) throw ..., and x ?? throw .... A code fixer is available.
  • KTSU0005 (Error): Orphaned PackageVersion entries. Flags PackageVersion entries in Directory.Packages.props (Central Package Management) that no project in the solution references via PackageReference/GlobalPackageReference. A code fixer removes the orphaned entry. Disable with <KtsuEnableOrphanedPackageVersionAnalysis>false</KtsuEnableOrphanedPackageVersionAnalysis>. An ignore list (Sdk.targets) keeps SDK-governed packages from being flagged even without a direct PackageReference: the KTSU0001 standard packages (Polyfill, System.Memory, System.Threading.Tasks.Extensions) and the Microsoft.Testing.Extensions.* runner family that test SDKs (e.g. MSTest.Sdk) inject into test projects (which the scan skips). Consumers can extend it via <KtsuOrphanedPackageVersionIgnore Include="..." />.
  • KTSU0006 (Error): Transitive package used directly. Flags use of a type or member that originates from a transitive package dependency when the project does not declare a direct PackageReference to it. A code fixer adds the PackageReference (and, under Central Package Management, a matching PackageVersion). Disable with <KtsuEnableTransitivePackageAnalysis>false</KtsuEnableTransitivePackageAnalysis>.

These properties are passed to analyzers via CompilerVisibleProperty: IsTestProject, TestProjectExists, TestProjectNamespace, TargetFramework, TargetFrameworkIdentifier, HasPolyfill, HasSystemMemory, HasSystemThreadingTasksExtensions, ManagePackageVersionsCentrally.

Package-graph analyzer inputs: KTSU0005 and KTSU0006 require solution-wide / post-restore facts that a per-project Roslyn analyzer cannot observe on its own. The SDK targets _KtsuGenerateOrphanedPackageVersionInputs and _KtsuGenerateTransitivePackageInputs (in Sdk/Sdk.targets) compute these facts at build time and surface them to the analyzers as AdditionalFiles (orphan list, assembly→package map, direct-package set), alongside the Directory.Packages.props and .csproj XML files that the code fixers edit via AdditionalDocuments.

Polyfill Configuration: For non-test projects, the SDK automatically sets:

  • PolyEnsure=true - Enables ensure/guard clause polyfills
  • PolyNullability=true - Enables nullability-related polyfills
  • PolyArgumentExceptions=true - Enables argument exception polyfills
  • PolyStringInterpolation=true - Enables string interpolation polyfills

Metadata File Integration

The SDK reads markdown files from the solution root and uses them to populate package metadata:

  • AUTHORS.md → Authors, AuthorsNamespace
  • VERSION.md → Version, PackageVersion
  • DESCRIPTION.md → Description, PackageDescription (checked in project directory first, then solution directory)
  • CHANGELOG.md → PackageReleaseNotes (truncated at 35KB if needed)
  • TAGS.md → Tags, PackageTags (checked in project directory first, then solution directory)
  • LICENSE.md → PackageLicenseFile
  • README.md → PackageReadmeFile (checked in project directory first, then solution directory)
  • COPYRIGHT.md → Copyright
  • PROJECT.url → ProjectUrl, PackageProjectUrl
  • AUTHORS.url → AuthorsUrl
  • icon.png → PackageIcon

All metadata files are automatically included in NuGet packages.

Important MSBuild Properties

Multi-Targeting

Default: net10.0;net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.0;netstandard2.1

Individual SDK sub-projects (ConsoleApp, App) override TargetFrameworks to target a single framework (net10.0).

Code Quality

  • LangVersion=latest
  • Nullable=enable
  • TreatWarningsAsErrors=true
  • AnalysisLevel=latest-all
  • EnforceCodeStyleInBuild=true

Package Validation

  • EnablePackageValidation=true
  • ApiCompatValidateAssemblies=true
  • EnableStrictModeForBaselineValidation=true — real breaking changes vs a published baseline are caught
  • EnableStrictModeForCompatibleFrameworksInPackage=false and EnableStrictModeForCompatibleTfms=false — strict cross-TFM validation is intentionally off. This SDK mandates Polyfill + broad multi-targeting, and Polyfill source-embeds framework shim types whose shape legitimately differs per TFM; strict mode reports those as false-positive breaking changes (CP0002/CP0014/CP0015/CP0016). Baseline validation stays on, and package consumers are unaffected (validation is producer-side only). Repos capture any residual non-strict compatible-framework diffs in a regenerable CompatibilitySuppressions.xml (dotnet pack -p:ApiCompatGenerateSuppressionFile=true).

Runtime Identifiers

Default RIDs: win-x64;win-x86;win-arm64;osx-x64;linux-x64;osx-arm64;linux-arm64

CI/CD Workflow

The GitHub Actions workflow (.github/workflows/dotnet-sdk.yml) runs on:

  • Push to main or develop branches
  • Pull requests
  • Nightly schedule (11 PM UTC)
  • Manual workflow dispatch

The workflow uses .NET SDK 10.0.

Release process (only on main branch, non-fork):

  1. Generate VERSION.md, LICENSE.md, CHANGELOG.md from git history
  2. Update analyzer releases with make-analyzer-releases.ps1
  3. Commit metadata changes with bot attribution
  4. Commit Sdk.props/Sdk.targets version updates
  5. Build all projects
  6. Run tests
  7. Create NuGet packages
  8. Publish to GitHub Packages, NuGet.org, and ktsu.dev package feeds
  9. Create GitHub release with artifacts

Common Development Tasks

Adding a New SDK Sub-Project

  1. Create directory: Sdk.{Name}/
  2. Create Sdk.{Name}.csproj with appropriate TargetFrameworks
  3. Create Sdk.props with project-type-specific property overrides
  4. Create Sdk.targets if custom build logic needed
  5. Package structure: SDK packages must include Sdk/Sdk.props and Sdk/Sdk.targets in the package

Modifying Core SDK Logic

  • Solution/project discovery: Edit Sdk/Sdk.props (lines 1-70)
  • Project type detection: Edit Sdk/Sdk.props (lines 72-187)
  • Metadata file loading: Edit Sdk/Sdk.props (lines 189-248)
  • Namespace generation: Edit Sdk/Sdk.props (lines 249-287)
  • Package configuration: Edit Sdk/Sdk.props (lines 289-330)
  • Package reference detection: Edit Sdk/Sdk.targets (lines 29-53)
  • Polyfill configuration: Edit Sdk/Sdk.targets (lines 76-82)

Testing SDK Changes Locally

  1. Build the SDK: dotnet build --configuration Release
  2. Pack the SDK: dotnet pack --configuration Release --output ./local-packages
  3. In consuming project, add local package source:
    <PropertyGroup>
      <RestoreAdditionalProjectSources>C:\dev\ktsu-dev\Sdk\local-packages</RestoreAdditionalProjectSources>
    </PropertyGroup>
  4. Reference the local version in consuming project's csproj or global.json

Architecture Notes

Modular Structure

The SDK projects (Sdk, Sdk.ConsoleApp, Sdk.App) use a modular architecture with shared configuration files:

  • Sdk.Common.SolutionDiscovery.props: Shared solution/project discovery logic
  • Sdk.Common.MetadataFiles.props: Shared metadata file loading logic
  • Sdk.Common.PackageProperties.props: Shared package configuration
  • Sdk.Common.SdkContent.targets: Shared SDK content packaging logic
  • Sdk.Common.PackageContent.targets: Shared package content inclusion logic

Each SDK project imports these modular files to avoid code duplication and ensure consistency.

MSBuild Evaluation Order

The SDK uses careful property evaluation to ensure correct values:

  1. Early evaluation: Solution discovery, file path resolution
  2. Mid evaluation: Metadata file reading, namespace calculation
  3. Late evaluation: Derived properties (IsExecutable, IsPackable, etc.)

Properties are set conditionally to avoid overwriting user-specified values.

Safe Array Operations

The SDK includes robust null/empty checks to prevent MSBuild failures:

  • Solution file array access uses .Split(';')[0] with validation
  • String operations check for null/empty before manipulation
  • File existence validated before File.ReadAllText() calls

Package Type

The core SDK sets PackageType=MSBuildSdk in Directory.Build.props, which is required for proper MSBuild SDK packaging. The Directory.Build.targets file includes the SDK props/targets files in the package at the correct paths.