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
56 changes: 47 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,70 @@ on:
- "*.*.*"

jobs:
# 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-rewriter/assembly-rewriter.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@v2
- uses: actions/checkout@v5
with:
fetch-depth: 1
- run: |
git fetch --prune --unshallow --tags
echo exit code $?
git tag --list
- uses: actions/setup-dotnet@v1
- uses: actions/setup-dotnet@v5
with:
dotnet-version: |
5.0.x
6.0.x
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.302'
source-url: https://nuget.pkg.github.com/nullean/index.json
dotnet-version: |
10.0.x
source-url: https://nuget.pkg.github.com/nullean/index.json
env:
NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

- run: ./build.sh build -s true
name: Build
- 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"
- run: ./build.sh generateapichanges -s true
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-rewriter"
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-rewriter.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("*.sln").Length = 0 do dir <- dir.Parent
Expand Down
65 changes: 59 additions & 6 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 All @@ -22,6 +23,9 @@ let private currentVersion =
o.Line
)

let private currentVersionInformational =
lazy (sprintf "%s+%s" currentVersion.Value (Information.getCurrentSHA1 "."))

let private clean (arguments:ParseResults<Arguments>) =
if (Paths.Output.Exists) then Paths.Output.Delete (true)
exec "dotnet" ["clean"] |> ignore
Expand All @@ -33,24 +37,73 @@ let private pristineCheck (arguments:ParseResults<Arguments>) =
| true -> printfn "The checkout folder does not have pending changes, proceeding"
| _ -> 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, "..", "rewriter-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"; currentVersion.Value; "-a"; Paths.ToolName; "-k"; "96c599bbe3e70f5d"] |> ignore
exec "dotnet" ["nupkg-validator"; nugetPackage; "-v"; currentVersionInformational.Value; "-a"; Paths.ToolName; "-k"; "96c599bbe3e70f5d"; "--allow-roll-forward"] |> ignore

let private generateApiChanges (arguments:ParseResults<Arguments>) =
let output = Paths.RootRelative <| Paths.Output.FullName
let currentVersion = currentVersion.Value
let args =
[
"assembly-differ"
(sprintf "previous-nuget|%s|%s|netcoreapp3.1" Paths.ToolName currentVersion);
(sprintf "directory|src/%s/bin/Release/netcoreapp3.1" Paths.ToolName);
(sprintf "previous-nuget|%s|%s|net10.0" Paths.ToolName currentVersion);
(sprintf "directory|src/%s/bin/Release/net10.0" Paths.ToolName);
"--target"; Paths.ToolName; "-f"; "github-comment"; "--output"; output
]

Expand Down
2 changes: 1 addition & 1 deletion build/scripts/scripts.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
]
},
"nupkg-validator": {
"version": "0.5.0",
"version": "0.10.1",
"commands": [
"nupkg-validator"
]
},
"assembly-differ": {
"version": "0.14.0",
"version": "0.16.0",
"commands": [
"assembly-differ"
]
Expand Down
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sdk": {
"version": "6.0.302",
"version": "10.0.100",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
}
86 changes: 86 additions & 0 deletions src/assembly-rewriter/AssemblyRewriterCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.ComponentModel.DataAnnotations;
using ILRepacking;
using Nullean.Argh;

namespace AssemblyRewriter;

internal sealed class AssemblyRewriterCommands
{
/// <summary>Rewrites assemblies and namespaces.</summary>
/// <param name="input">-i, --in, Input path for assembly to rewrite. Use multiple flags for multiple input paths.</param>
/// <param name="output">-o, --out, Output path for rewritten assembly. Use multiple flags for multiple output paths.</param>
/// <param name="resolveDir">-r, --resolvedir, Additional assembly resolve directories. Use multiple flags for multiple resolve directories.</param>
/// <param name="keyFile">-k, --keyfile, Sign rewritten assembly with this key file. When merge option is specified, the merged assembly will be signed.</param>
/// <param name="merge">-m, --merge, Merge all rewritten assemblies into a single assembly using the first output path as target.</param>
/// <param name="verbose">-v, --verbose, Verbose output.</param>
[DefaultCommand]
public int Rewrite(
[MinLength(1)] List<string> input,

Check warning on line 18 in src/assembly-rewriter/AssemblyRewriterCommand.cs

View workflow job for this annotation

GitHub Actions / AOT pack (linux-x64)

Using member 'System.ComponentModel.DataAnnotations.MinLengthAttribute.MinLengthAttribute(Int32)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.

Check warning on line 18 in src/assembly-rewriter/AssemblyRewriterCommand.cs

View workflow job for this annotation

GitHub Actions / AOT pack (linux-x64)

Using member 'System.ComponentModel.DataAnnotations.MinLengthAttribute.MinLengthAttribute(Int32)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.
[MinLength(1)] List<string> output,

Check warning on line 19 in src/assembly-rewriter/AssemblyRewriterCommand.cs

View workflow job for this annotation

GitHub Actions / AOT pack (linux-x64)

Using member 'System.ComponentModel.DataAnnotations.MinLengthAttribute.MinLengthAttribute(Int32)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.

Check warning on line 19 in src/assembly-rewriter/AssemblyRewriterCommand.cs

View workflow job for this annotation

GitHub Actions / AOT pack (linux-x64)

Using member 'System.ComponentModel.DataAnnotations.MinLengthAttribute.MinLengthAttribute(Int32)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.
List<string>? resolveDir = null,
string? keyFile = null,
bool merge = false,
bool verbose = false)
{
if (input.Count != output.Count)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Number of input paths must equal number of output paths");
Console.ResetColor();
return 1;
}

var options = new Options
{
InputPaths = input,
OutputPaths = output,
ResolveDirectories = resolveDir ?? [],
KeyFile = keyFile,
Merge = merge,
Verbose = verbose
};

try
{
var rewriter = new AssemblyRewriter(options);
rewriter.Rewrite(options.InputPaths, options.OutputPaths, options.ResolveDirectories);
}
catch (Exception e)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(e);
Console.ResetColor();
return 1;
}

if (!merge) return 0;

try
{
var repackOptions = new RepackOptions
{
Internalize = true,
Closed = true,
KeepOtherVersionReferences = false,
TargetKind = ILRepack.Kind.SameAsPrimaryAssembly,
InputAssemblies = output.ToArray(),
LineIndexation = true,
OutputFile = output.First(),
KeyFile = keyFile,
SearchDirectories = output.Select(p => new DirectoryInfo(p).FullName).Distinct(),
};

var pack = new ILRepack(repackOptions, new RepackConsoleLogger());
pack.Repack();
}
catch (Exception e)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(e);
Console.ResetColor();
return 2;
}

return 0;
}
}
9 changes: 1 addition & 8 deletions src/assembly-rewriter/Options.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
using System.Collections.Generic;
using CommandLine;

namespace AssemblyRewriter
{
public class Options
{
[Option('i', "in", Min = 1, Required = true, HelpText = "input path for assembly to rewrite. Use multiple flags for multiple input paths")]
public IEnumerable<string> InputPaths { get; set; }

[Option('o', "out", Min = 1, Required = true, HelpText = "output path for rewritten assembly. Use multiple flags for multiple output paths")]
public IEnumerable<string> OutputPaths { get; set; }

[Option('r', "resolvedir", HelpText = "Additional assembly resolve directories. Use multiple flags for multiple resolve directories")]
public IEnumerable<string> ResolveDirectories { get; set; }
public IEnumerable<string> ResolveDirectories { get; set; } = [];

[Option('k', "keyfile", HelpText = "Sign rewritten assembly with this key file. When merge option is specified, the merged assembly will be signed.")]
public string KeyFile { get; set; }

[Option('m', "merge", Default = false, HelpText = "Merge all rewritten assemblies into a single assembly using the first output path as target")]
public bool Merge { get; set; }

[Option('v', "verbose", Default = false, HelpText = "verbose output")]
public bool Verbose { get; set; }
}
}
Loading
Loading