Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,46 @@ on:
- "*.*.*"

jobs:
# Native AOT can only compile for the OS/arch it runs on, so each leg compiles its own RID and
# packs the per-RID tool package. The coordinating build job below takes only the root package and
# the portable 'any' fallback from a plain `dotnet pack` — see generatePackages in
# build/scripts/Targets.fs. On pull requests only linux-x64 runs, to prove AOT still links; the
# other four RIDs get the same proof on every push.
# On pull requests, only linux-x64 runs, to prove AOT still links without burning the full matrix
# on packages nobody sees. All five run on push, where the packages are actually uploaded.
aot-pack:
runs-on: ${{ matrix.runner }}
name: AOT pack (${{ matrix.rid }})
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(github.event_name == 'pull_request'
&& '[{"rid":"linux-x64","runner":"ubuntu-latest"}]'
|| '[{"rid":"linux-x64","runner":"ubuntu-latest"},{"rid":"linux-arm64","runner":"ubuntu-24.04-arm"},{"rid":"win-x64","runner":"windows-latest"},{"rid":"win-arm64","runner":"windows-11-arm"},{"rid":"osx-arm64","runner":"macos-latest"}]') }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 1
- uses: actions/setup-dotnet@v5
with:
dotnet-version: |
10.0.x

- run: dotnet pack src/assembly-differ/assembly-differ.csproj -c Release -r ${{ matrix.rid }} -o build/output
name: Pack native-AOT tool for ${{ matrix.rid }}
shell: bash

- name: Upload per-RID package
if: github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: nupkg-${{ matrix.rid }}
path: build/output/*.nupkg
if-no-files-found: error

build:
runs-on: ubuntu-latest
needs: aot-pack
steps:
- uses: actions/checkout@v5
with:
Expand All @@ -38,7 +76,15 @@ jobs:

- run: ./build.sh generatepackages -s true
name: Generate local nuget packages


- name: Download per-RID AOT packages
if: github.event_name == 'push'
uses: actions/download-artifact@v4
with:
pattern: nupkg-*
path: build/output
merge-multiple: true

# - run: ./build.sh validatepackages -s true
# name: "validate *.npkg files that were created"

Expand Down
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Distributed as a .NET tool so install using the following
dotnet tool install assembly-differ
```

On Linux, Windows and macOS/arm64, this resolves to a self-contained native-AOT executable — no
shared .NET runtime required, and no first-run JIT warmup. Everywhere else, it falls back to a
framework-dependent build (requires the .NET runtime the tool targets to already be installed).

## Run

```bat
Expand All @@ -32,7 +36,7 @@ You can omit `dotnet` if you install this as a global tool
to see the supported Assembly Providers and outputs:

```bat
assembly-differ <Old Assembly Provider> <New Assembly Provider> [Options]
assembly-differ diff <Old Assembly Provider> <New Assembly Provider> [Options]

Supported Assembly Providers:

Expand All @@ -43,32 +47,45 @@ Supported Assembly Providers:
github|<owner>/<repo>|<commit>|<build command>|<relative output path>

Options:
-t, --target=VALUE the assembly targets. Defaults to *all* assemblies
located by the provider
-f, --format=VALUE the format of the diff output. Supported formats
are xml, markdown, asciidoc. Defaults to xml
-o, --output=VALUE the output directory. Defaults to current directory
-h, -?, --help show this message and exit
-t, --target <values> the assembly targets. Defaults to *all* assemblies
located by the provider. May be given more than once.
-f, --format <string> the format of the diff output. Supported formats
are xml, markdown, asciidoc. Defaults to xml
-o, --output <string> the output directory or file name. If not specified only prints to console
-p, --prevent-change <string> fail if the change detected is higher than specified:
none, patch, minor, or major. Defaults to none
-a, --allow-empty-previous-nuget don't fail when no previous nuget package could be found to diff against
-h, --help show this message and exit
```

> [!NOTE]
> Starting from `1.0.0`, the tool moved off `Mono.Options` onto [`Nullean.Argh`](https://github.com/nullean/argh)
> for CLI parsing, which introduces an explicit `diff` subcommand alongside the bare invocation. `diff` is
> optional as long as an option comes before the two provider arguments (e.g.
> `assembly-differ --target NEST "nuget|..." "nuget|..."`); providers as the very first arguments
> (`assembly-differ "nuget|..." "nuget|..."`) still need the explicit subcommand
> (`assembly-differ diff "nuget|..." "nuget|..."`), since the CLI parser resolves a bare leading argument
> as a subcommand name first. `--target` also moves from a single comma/pipe-separated value to a
> repeatable flag (`--target a --target b` instead of `--target a,b`).

#### Examples:

Diff between two local assemblies:

```bat
dotnet assembly-differ "assembly|C:\6.1.0\Nest.dll" "assembly|C:\6.2.0\Nest.dll"
dotnet assembly-differ diff "assembly|C:\6.1.0\Nest.dll" "assembly|C:\6.2.0\Nest.dll"
```

Diff between all assemblies in directories, matched by name:

```bat
dotnet assembly-differ "directory|C:\6.1.0" "directory|C:\6.2.0"
dotnet assembly-differ diff "directory|C:\6.1.0" "directory|C:\6.2.0"
```

Diff NuGet packages:

```bat
dotnet assembly-differ "nuget|NEST|6.1.0|net46" "nuget|NEST|6.2.0|net46"
dotnet assembly-differ diff "nuget|NEST|6.1.0|net46" "nuget|NEST|6.2.0|net46"
```

Diff Previous NuGet packages:
Expand All @@ -77,16 +94,16 @@ Imagine you want to release `6.2.0` and want to diff with whatever is the latest
`previous-nuget` will do the heavy lifting of finding that previous release

```bat
dotnet assembly-differ "previous-nuget|NEST|6.2.0|net46" "directory|C:\6.2.0"
dotnet assembly-differ diff "previous-nuget|NEST|6.2.0|net46" "directory|C:\6.2.0"
```

Diff GitHub commits:

```bat
dotnet assembly-differ "github|elastic/elasticsearch-net|6.1.0|cmd /C call build.bat skiptests skipdocs|build\output\Nest\net46" "github|elastic/elasticsearch-net|6.2.0|cmd /C call build.bat skiptests skipdocs|build\output\Nest\net46"
dotnet assembly-differ diff "github|elastic/elasticsearch-net|6.1.0|cmd /C call build.bat skiptests skipdocs|build\output\Nest\net46" "github|elastic/elasticsearch-net|6.2.0|cmd /C call build.bat skiptests skipdocs|build\output\Nest\net46"
```

Any of the above can be mixed. For example, to compare GitHub HEAD against last NuGet package, and output in Markdown
Any of the above can be mixed. For example, to compare GitHub HEAD against last NuGet package, and output in Markdown — note that leading with an option lets you drop the `diff` subcommand:

```bat
dotnet assembly-differ --format markdown "nuget|NEST|6.2.0|net46" "github|elastic/elasticsearch-net|HEAD|cmd /C call build.bat skiptests skipdocs|build\output\Nest\net46"
Expand All @@ -98,7 +115,7 @@ You can run the tool locally against itself using the following during developme

```bat
dotnet build -c Release
dotnet run --framework netcoreapp3.1 -- "previous-nuget|assembly-differ|0.9.1|netcoreapp3.1" "directory|bin/Release/netcoreapp3.1" --target=assembly-differ
dotnet run -f net10.0 -- diff "previous-nuget|assembly-differ|0.9.1|net10.0" "directory|bin/Release/net10.0" --target assembly-differ
```

# FUTURE PLANS
Expand Down
8 changes: 8 additions & 0 deletions build/scripts/Paths.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ open System.IO
let ToolName = "assembly-differ"
let Repository = sprintf "nullean/%s" ToolName

/// The RIDs we ship native-AOT tool packages for. AOT compilation requires a matching
/// OS/arch, so CI packs one RID per runner; this list only documents the set.
let AotRuntimeIdentifiers = ["linux-x64"; "linux-arm64"; "win-x64"; "win-arm64"; "osx-arm64"]

/// Must mirror assembly-differ.csproj's TargetFrameworks. Used to patch the signed managed dll back
/// into the packed 'any' fallback for every TFM it ships — see fixAnyPackageSigning in Targets.fs.
let ManagedTargetFrameworks = ["net8.0"; "net10.0"]

let Root =
let mutable dir = DirectoryInfo(".")
while dir.GetFiles("*.slnx").Length = 0 do dir <- dir.Parent
Expand Down
57 changes: 54 additions & 3 deletions build/scripts/Targets.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Targets
open Argu
open System
open System.IO
open System.IO.Compression
open Bullseye
open CommandLine
open Fake.Tools.Git
Expand Down Expand Up @@ -36,13 +37,62 @@ let private pristineCheck (arguments:ParseResults<Arguments>) =
()
//failwithf "The checkout folder has pending changes, aborting"

let private isPerRidPackage (name: string) =
Paths.AotRuntimeIdentifiers |> List.exists (fun rid -> name.Contains(sprintf ".%s." rid))

/// `dotnet pack`'s RID-aware tool-packaging path (used once RuntimeIdentifiers is declared) copies
/// the *unsigned* obj/ build of this project's own assembly into the portable 'any' package, even
/// though the normal bin/ output is correctly strong-name signed — a long-standing obj-vs-bin mixup
/// in `dotnet pack` (see https://github.com/dotnet/sdk/issues/20197) that resurfaces here. Patched in
/// place after packing by swapping in the signed bin/ copies for every TFM the 'any' package ships.
let private fixAnyPackageSigning (anyPackagePath: string) =
use archive = ZipFile.Open(anyPackagePath, ZipArchiveMode.Update)
for tfm in Paths.ManagedTargetFrameworks do
let entryName = sprintf "tools/%s/any/%s.dll" tfm Paths.ToolName
let signedDll = Path.Combine(Paths.ToolProject.FullName, "bin", "Release", tfm, sprintf "%s.dll" Paths.ToolName)
match archive.GetEntry(entryName), File.Exists signedDll with
| null, _ | _, false -> ()
| entry, true ->
entry.Delete()
let newEntry = archive.CreateEntry(entryName)
use entryStream = newEntry.Open()
use fileStream = File.OpenRead(signedDll)
fileStream.CopyTo(entryStream)

let private generatePackages (arguments:ParseResults<Arguments>) =
let output = Paths.RootRelative Paths.Output.FullName
exec "dotnet" ["pack"; "-c"; "Release"; "-o"; output] |> ignore

if not Paths.Output.Exists then Paths.Output.Create()

// A plain `dotnet pack` emits the root package (whose DotnetToolSettings.xml v2 maps each RID to
// its own package) AND a package per RID — but native AOT can only compile for the machine it
// runs on, so those per-RID outputs from a single machine are self-contained MANAGED builds,
// silently missing the AOT compilation. We therefore keep only the root and the portable 'any'
// fallback here, and take the real per-RID packages from the CI matrix, where each is compiled
// on a matching runner (see aot-pack in .github/workflows/ci.yml).
let staging = Paths.RootRelative <| Path.Combine(Paths.Output.FullName, "..", "differ-staging")
if Directory.Exists staging then Directory.Delete(staging, true)
exec "dotnet" ["pack"; sprintf "src/%s/%s.csproj" Paths.ToolName Paths.ToolName; "-c"; "Release"; "-o"; staging] |> ignore

DirectoryInfo(staging).GetFiles("*.nupkg")
|> Seq.filter (fun f -> not (isPerRidPackage f.Name))
|> Seq.iter (fun f ->
let destination = Path.Combine(Paths.Output.FullName, f.Name)
printfn "keeping %s" f.Name
f.CopyTo(destination, true) |> ignore
if f.Name.Contains(sprintf "%s.any." Paths.ToolName) then
fixAnyPackageSigning destination)

Directory.Delete(staging, true)

let private validatePackages (arguments:ParseResults<Arguments>) =
let nugetPackage =
let p = Paths.Output.GetFiles "*.nupkg" |> Seq.sortByDescending(fun f -> f.CreationTimeUtc) |> Seq.head
// Only the 'any' package carries a signed managed assembly to check: the root package is
// just a DotnetToolSettings.xml pointer with no dll of its own, and the per-RID AOT packages
// hold a native binary with no managed identity either.
let p =
Paths.Output.GetFiles "*.nupkg"
|> Seq.filter (fun f -> f.Name.Contains(sprintf "%s.any." Paths.ToolName))
|> Seq.sortByDescending(fun f -> f.CreationTimeUtc) |> Seq.head
Paths.RootRelative p.FullName
exec "dotnet" ["nupkg-validator"; nugetPackage; "-v"; currentVersionInformational.Value; "-a"; Paths.ToolName; "-k"; "96c599bbe3e70f5d"; "--allow-roll-forward"] |> ignore

Expand All @@ -53,6 +103,7 @@ let private generateApiChanges (arguments:ParseResults<Arguments>) =
let dotnetRun =[ "run"; "-c"; "Release"; "-f"; "net10.0"; "--project"; project]
let args =
[
"diff";
sprintf "previous-nuget|%s|%s|net8.0" Paths.ToolName currentVersion;
sprintf "directory|src/%s/bin/Release/net10.0" Paths.ToolName;
"--target"; Paths.ToolName; "-f"; "github-comment"; "--output"; output
Expand Down
Loading
Loading