Skip to content

build: rework the build system and project layout#2367

Draft
mjcheetham wants to merge 34 commits into
git-ecosystem:vnextfrom
mjcheetham:newbuild
Draft

build: rework the build system and project layout#2367
mjcheetham wants to merge 34 commits into
git-ecosystem:vnextfrom
mjcheetham:newbuild

Conversation

@mjcheetham

Copy link
Copy Markdown
Contributor

Summary

A top-to-bottom rework of how the repository is laid out and built. Flattens the src/shared tree into src/, renames the executable to git-credential-manager, and replaces the per-platform packaging projects with a uniform build/<os> layout driven by thin scripts over a
shared CLI scripting library. Migrates the solution to SLNX, adopts the .NET 10 artifacts-output layout, refreshes CI and docs, and restores the .NET tool package.

Why

The src/{windows,osx,linux} + src/shared split is a relic from when code was platform-partitioned. Every library is cross-platform now, so the structure only adds noise, and the old monolithic layout.sh / pack.sh packaging was hard to follow and inconsistent across platforms. This branch makes the layout flat and the packaging uniform, and brings the build onto current .NET conventions (SLNX, UseArtifactsOutput, NoTargets distribution projects).

What changes

Project layout

  • Flatten all libraries and tests up one level into src/ (pure move — identical assemblies); relocate the scoped Directory.Build.props.
  • Rename the executable project Git-Credential-Managergit-credential-manager so project, directory and output binary all match the command name.
  • Move the MSBuild tasks under build/msbuild (renamed MSBuildTasks).

Packaging (uniform build/<os>)

  • Add shared CLI scripting libraries (lib-cli.sh, lib-cli.psm1): logging, path/version/config resolution, flag parsing.
  • Rework Linux, macOS and Windows packaging into build/<os> with a *.Distribution.csproj (NoTargets) anchoring each in the solution plus thin publish / pack / archive scripts.
  • Bump Inno Setup to 6.7.3; fetch the compiler on demand via a download helper instead of from the NuGet package path.

Solution & .NET 10 config

  • Migrate to the SLNX solution format (terser, merges cleanly, no GUID bookkeeping); rename the ReSharper DotSettings sidecar to match.
  • Adopt UseArtifactsOutput (everything under out/), default to net10.0, bump the SDK to 10.0, register Microsoft.Build.NoTargets, normalise VERSION.
  • Mark the three *.Distribution.csproj <Build Project="false" /> so a plain dotnet build/dotnet test builds only the product and tests — no accidental publish-and-package side effects. They still build directly via dotnet build build/<os>.

install-from-source

  • Promote the Linux-only script to a cross-platform build/install-from-source.sh: detect OS, bootstrap deps, publish via the per-OS script, stage under <prefix>/share/gcm-core with a <prefix>/bin launcher symlink.
  • Default to a trimmed, self-contained non-AOT build (needs only the .NET SDK); --aot opts into the native toolchain. sudo only when the prefix isn't writable. Fixes two latent bugs from the old script (multi-SDK detection, cwd change breaking repo detection).
  • Add a --aot / --no-aot toggle to the publish scripts (defaults to AOT, so the shipped package is unchanged).

.NET tool

  • Restore git-credential-manager .NET tool packaging under build/dntool (framework-dependent IL; pack the code-signed layout with --no-build).

CI & docs

  • Point Azure Pipelines and GitHub Actions at the new build/ entry points and flattened paths; add the ESRP sign.yml template; add jobs to build and (on release) sign + publish the .NET tool; validate install-from-source on the new path and on macOS.
  • Refresh the development guide for the new layout and outputs; document building each platform's distributables and the .NET tool; note the Homebrew-SDK Native-AOT link caveat.

Notes

  • Despite the size, the change is overwhelmingly moves, renames and build plumbing; library contents and the produced binaries are unchanged.
  • A solution build no longer triggers packaging — produce distributables with dotnet build build/<os>.

Testing

  • dotnet build git-credential-manager.slnx (product + tests only)
  • Core + platform test suites
  • Distribution builds via dotnet build build/<os>
  • install-from-source validated on Linux matrix + macOS

Requires PR #2366 to be completed first.

Drop the .NET Framework target (net472) from all projects and build
scripts. Windows builds will now target the, only, TFM of `net10.0`.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Remove all code that was conditional on a .NET Framework target. Now
that we're only building against the CoreCLR, this code was dead.

Note that we now lose the ability to use the embedded web view for
Microsoft authentication, which was a .NET Framework-only feature (and
therefore Windows only). Support for embedded web view flows will be
restored at a later date via Avalonia WebView.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add the SupportedOSPlatform attributes to types that only run on the
relevant specific OS platform. This will help ensure we're always gating
use of OS-specific behaviour to that specific OS.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Suppress the "This call site is reachable on all platforms" warning
for all test projects - we use custom Xunit attributes to dynamically
skip OS-specific tests unless the test is running on the appropriate OS.
The analyzer for CA1416 does not know how to handle this.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Upgrade all Avalonia packages to the latest version, which is 12.0.5 at
time of writing.

Version 12.x has some breaking changes that we must react to, including
a required and explicit `x:DataType` specified for the now default
compiled bindings in all views. There have also been changes to how the
clipboard is accessed, as well as hotkeys. Finally some properties have
been renamed and reworked, specifically on the customising window
chrome/decorations and control placeholder text.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
In order to support ahead-of-time (AOT) compilation we must avoid using
reflection-based APIs. JSON serialisation is one of those APIs that we
use a lot across the product.

System.Text.Json supports source generation-based serialisation. We opt
into this by creating explict 'JSON contexts' for each area that define
the set of types we should generate serialisers for, including custom
serialisation options (case sensitivity, naming conventions, etc).

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Enable JSON source generation for Trace2 messages. This will further
help unlock AOT compilation options which cannot use reflection-based
APIs.

Replace the custom JSON naming policy with the built-in lower snake case
policy, and for good measure also explictly set the enum string values
as attributes on the members themselves.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Enable AOT compilation on publish for Git Credential Manager by default!

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
@mjcheetham mjcheetham added engineering Refactoring or build changes gcm3.0 Targets the next major release of Git Credential Manager - v3.0 labels Jun 29, 2026
@mjcheetham mjcheetham changed the title build: rework the build system and project lay build: rework the build system and project layout Jun 29, 2026
@mjcheetham mjcheetham force-pushed the newbuild branch 2 times, most recently from f4fd0c4 to d1b00a2 Compare June 29, 2026 15:51
Enabling Native AOT makes the Linux CI jobs link a native binary for
the target runtime. Cross-linking the Arm runtimes from the x64
ubuntu-latest runner has no target toolchain, so the linux-arm job
fails at the clang link step.

Run the Arm matrix entries on GitHub's Arm64 runners so linux-arm64
links natively, and install the armhf GNU toolchain
(gcc-arm-linux-gnueabihf, which pulls in binutils and the sysroot) for
the 32-bit linux-arm target, which still cross-compiles on the Arm64
host.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Native AOT links the published binary with clang. Leaving it on for a
source install would force every distro to install a C toolchain just
to build a local copy, which is why the install-from-source validation
fails across the board with "gcc or clang is missing".

Installing from source does not need a native binary, so turn AOT off
for it alone: set PublishAot=false in the environment for the build
(the executable project honours an externally supplied value, and the
nested publish inherits it through the packaging scripts). With AOT
off, layout.sh's existing self-contained, single-file publish produces
a portable binary that needs only the .NET SDK to build and bundles its
own runtime to run - so it launches even on the distros where the SDK
was installed to a non-standard location. The shipped packages set
nothing and still default to AOT.

This is a deliberate stopgap that leaves the rest of the
install-from-source flow as-is; a later commit reworks
install-from-source handling altogether.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The src/shared directory is a relic of when code was split between
platform-specific src/{windows,osx,linux} trees and a separate shared
tree. Every library and test project is cross-platform now, so the
extra "shared" level only adds noise to project paths and solution
entries.

Lift all libraries and their test projects up one level into src/.
This is a pure move: project contents and their relative references to
one another are unchanged, so the assemblies that get built are
identical.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
With the shared projects flattened into src/, move the
Directory.Build.props that scoped them up to src/ so it keeps applying
to exactly the same set of projects.

Drop the manual PlatformOutPath/BaseOutputPath overrides while here:
output redirection is being centralized via UseArtifactsOutput in the
root props (see later commit), so per-tree output paths are no longer
needed. Only the chained import of the repository-root props remains.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Rename the executable project from Git-Credential-Manager to
git-credential-manager so the project, its directory and the output
binary all match the git-credential-manager command name and the
lowercase naming used across the rest of the build.

This is a pure move; the project file is unchanged apart from its
path and filename casing.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The DotnetTool project existed only to repackage the executable as a
.NET tool, carrying its own csproj, DotnetToolSettings.xml, nuspec and
layout/pack scripts alongside the real project.

Its packaging responsibilities move into the reworked build system
introduced in the following commits, so remove the project. Preserve the
tool icon by moving it next to the executable project.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Collect the custom MSBuild tasks - GetVersion and
GenerateWindowsAppManifest - together with their project and the .tasks
import file under a dedicated build/msbuild/ directory, and rename the
project to MSBuildTasks.

This frees the top level of build/ for the platform packaging that
follows and gives the tasks assembly a clearer home. Point the root
Directory.Build.targets at the new import path and thread a
VersionOverride through to the GetVersion task.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add lib-cli.sh and its PowerShell counterpart lib-cli.psm1: small
helper libraries that the per-platform build/publish/pack/archive scripts
introduced next will source. They centralize logging, repository and
artifacts path resolution, and runtime/version/configuration
normalisation (plus Inno Setup discovery on Windows).

Keeping this logic in one place per shell keeps the platform scripts
thin and consistent, and gives them a single source of truth for things
like the verbose-output flag and the out/ layout.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Replace the old src/linux/Packaging.Linux project and its
build/layout/pack scripts with a build/linux layout. A
Linux.Distribution.csproj (a NoTargets project) anchors packaging in the
solution, while thin publish/pack/archive scripts built on the shared
lib-cli library do the actual work, replacing the previous monolithic
layout.sh and pack.sh.

The Debian control file moves under debian-package/, and
install-from-source.sh carries over unchanged.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
@mjcheetham mjcheetham force-pushed the newbuild branch 3 times, most recently from e459113 to 0ae7fc4 Compare June 29, 2026 16:45
Replace src/osx/Installer.Mac with a build/macos layout mirroring the
Linux one: a Mac.Distribution.csproj (NoTargets) plus
publish/pack/archive/codesign scripts and an import-developer-certificate
helper, all built on the shared lib-cli library. This subsumes the old
build/layout/pack/dist/notarize/codesign scripts.

The installer's distribution XML, resources, postinstall script,
entitlements and uninstall.sh move under build/macos, with the .pkg
pieces grouped under installer/.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Replace src/windows/Installer.Windows with a build/windows layout
matching the other platforms: a Windows.Distribution.csproj (NoTargets)
plus publish/pack/archive PowerShell scripts built on lib-cli.psm1, and a
download-innosetup helper that fetches the Inno Setup compiler on demand.
The Setup.iss installer script moves under installer/.

Bump the Inno Setup package to 6.7.3 and drop its now-unused
GeneratePathProperty: the compiler is resolved by the download helper at
build time rather than from the NuGet package path.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Replace the legacy MSBuild .sln with the newer .slnx solution format.
SLNX is terser, diffs and merges far more cleanly, and lists projects by
path without the GUID bookkeeping of the old format. The new solution
references the flattened src/ projects and the build/ distribution
projects in their new homes.

Rename the ReSharper DotSettings sidecar to match the new lowercase
solution name so it keeps applying.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Turn on UseArtifactsOutput so every project's binaries and
intermediates land under out/ in the standard .NET artifacts layout
(replacing the per-tree output paths removed earlier), and default all
projects to net10.0 - individual projects may still override.

Bump the SDK in global.json to 10.0 and register the
Microsoft.Build.NoTargets MSBuild SDK used by the new distribution
projects. Normalise VERSION to the three-part 3.0.0.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Point the Azure Pipelines release definition and the GitHub Actions
workflows at the new build/ script entry points (publish/pack/archive,
import-developer-certificate) and the flattened project paths, replacing
references to the old src/{linux,osx,windows} packaging projects and
scripts.

Add the ESRP sign.yml template that the release pipeline invokes to
code-sign the Windows and macOS artifacts.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Refresh the development guide to match the reworked build: refer to
git-credential-manager.slnx, document building each platform's
distributables through the build/<os> projects with --configuration and
--runtime, and point at the new out/package and out/publish output
locations.

Add a note explaining why building the macOS distribution with
Homebrew's non-portable .NET SDK fails at the Native AOT link step, and
recommend using the official SDK instead.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The Linux, Mac and Windows *.Distribution.csproj projects each run
their platform build.sh / publish pipeline from a target that fires
AfterTargets="Build". Because they sat in the solution default build
set, a plain `dotnet build` (or `dotnet test`) on
git-credential-manager.slnx kicked off the full publish-and-package
pipeline on the matching OS - slow, and with side effects no inner-loop
build should have.

Mark the three distribution projects with <Build Project="false" />
so a solution build covers just the product and its tests. They stay in
the solution (visible and loadable in IDEs) and are still built directly
via `dotnet build build/<os>`, which is how CI and a deliberate
distribution build invoke them.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Publishing always produced an ahead-of-time (AOT) build, since the
project sets PublishAot=true. AOT needs a native toolchain (clang, zlib)
to link, which is fine for the shipped package but a heavy requirement
for building from source.

Add an --aot / --no-aot toggle to the Linux and macOS publish.sh
scripts (and an equivalent -Aot switch to the Windows publish.ps1),
defaulting to AOT so the shipped package is unchanged. --no-aot publishes
a trimmed, self-contained build that needs only the .NET SDK.

The boolean --x / --no-x parsing lives in a new bool_flag helper in
lib-cli.sh for reuse. Trimming for the non-AOT build is configured in the
project (PublishTrimmed, gated on PublishAot being off) rather than passed
on the command line; the product is already AOT-safe, hence trim-safe.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The Linux install-from-source script built through the deleted
src/linux/Packaging.Linux project (its InstallFromSource MSBuild path)
and detected a checkout using the old src/linux paths and the .sln file,
so it stopped working after the build-system rework.

Promote it to a single cross-platform build/install-from-source.sh:
detect the OS, bootstrap dependencies (Linux distro package managers, or
git plus the .NET SDK on macOS), publish through the per-OS
build/<os>/publish.sh, and stage the result under <prefix>/share/gcm-core
with a launcher symlink in <prefix>/bin - the same layout the .deb uses.

Default to a trimmed, self-contained non-AOT build (needs only the
.NET SDK); --aot opts into a native build (and the clang/zlib toolchain
on Linux). Elevate the install with sudo only when the prefix is not
writable, so user-prefix installs need no root.

Also fix two latent bugs carried over from the old script: the
SDK-detection test broke when multiple SDKs were installed (forcing a
needless dotnet re-download), and the dotnet bootstrap changed the
working directory, which broke repo detection.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The validate-install-from-source workflow invoked the script from its
old src/linux/Packaging.Linux location, which no longer exists after the
rework. Point it at the new build/install-from-source.sh.

Add a macOS job so the cross-platform script is exercised on macOS as
well as the Linux distro matrix. The default non-AOT build needs only the
.NET SDK, so it runs without a native toolchain on the runner.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The install-from-source script moved from src/linux/Packaging.Linux to
a cross-platform build/install-from-source.sh and gained an --aot toggle
that defaults to a trimmed, self-contained non-AOT build.

Fix the now-broken script link in the uninstall guide, document the
--aot option, and note that the script also runs on macOS.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The Install/Update/Uninstall subsections under the "## .NET tool"
heading were h4 (####) while their parent is h2, leaving a gap in the
heading hierarchy. Promote them to h3 (###) to match the sibling
sections.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The standalone DotnetTool project was dropped during the build-system
rework. Restore the ability to produce the git-credential-manager .NET tool
NuGet package, placing it under build/dntool to match the per-platform build
layout.

publish.sh publishes the application as portable, framework-dependent IL
(no AOT, trimming, runtime identifier or apphost); pack.sh then runs dotnet
pack with the no-build option, pointing PublishDir at the published layout so
that assemblies code-signed between the two steps are packaged as-is. The SDK
generates DotnetToolSettings.xml and a symbol package, so no hand-written
nuspec is required.

Dntool.Distribution.csproj is a NoTargets project that carries only the
tool and package metadata; it is driven by the scripts and marked no-build in
the solution, like the other distribution projects.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add a job to the CI workflow that builds the git-credential-manager .NET
tool package (unsigned) via build/dntool/build.sh and uploads the resulting
.nupkg/.snupkg, so the tool packaging is exercised on every push alongside the
per-platform distributables.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The .NET tool was dropped from the release pipeline during the
build-system rework, leaving a commented stub that referenced the removed
src/shared/DotnetTool scripts.

Add a dotnet_tool build job that publishes the framework-dependent IL via
build/dntool/publish.sh, ESRP-signs the managed assemblies (SigntoolSign),
then packs the signed layout into the .nupkg/.snupkg via build/dntool/pack.sh.
Re-enable the NuGet publish job to push the package.

Signing is performed by the ESRP service rather than the agent, so the
build job runs on the existing Linux pool alongside the other platform
builds.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add a .NET tool section to the development guide: how to build the package
with build/dntool/build.sh (it is script-built rather than via dotnet build,
being platform-agnostic) and how to install the result into an isolated tool
path to try it.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The Windows release jobs publish the executable with Native AOT, which
links the final binary with the MSVC linker (link.exe). The 1ES hosted
images do not ship the C++ toolchain, so every Windows leg fails with
"Platform linker not found" from Microsoft.NETCore.Native.Windows.targets.

Add a setup script that installs VS 2022 Build Tools with the single
VC.Tools component for the agent's architecture - x86.x64 on the Intel
legs (win-x86, win-x64), ARM64 on the Arm leg (win-arm64) - and run it
before the publish step. Installing just the architecture's component
keeps the download to the minimum that still provides link.exe and the
MSVC libraries; the .NET ILCompiler then discovers the toolchain through
the Visual Studio setup API.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engineering Refactoring or build changes gcm3.0 Targets the next major release of Git Credential Manager - v3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant