Skip to content

Cross-platform satellite assemblies via in-proc Csc task (fixes #82)#123

Open
paulirwin wants to merge 3 commits into
NightOwl888:mainfrom
paulirwin:issue/82
Open

Cross-platform satellite assemblies via in-proc Csc task (fixes #82)#123
paulirwin wants to merge 3 commits into
NightOwl888:mainfrom
paulirwin:issue/82

Conversation

@paulirwin

Copy link
Copy Markdown
Collaborator

Generate the ICU4N resource-only satellite assemblies with the in-process Csc MSBuild task (from Microsoft.Build.Tasks.CodeAnalysis, shipped with every modern dotnet SDK) — the same pattern the .NET SDK itself uses in Microsoft.NET.Sdk.targets::CoreGenerateSatelliteAssemblies. This replaces the prior al.exe / Mono al pipeline, which required the Windows SDK or Mono to build the netstandard2.0 target. Build now runs on Windows, macOS, and Linux with no extra tooling.

Verified: 220 satellites (1 neutral + 219 localized) build on macOS (SDK 10.0.201) and in Linux Docker (SDK 9.0.313), are strong-name signed with the ICU4N public key token, stamp version 60.0.0.0 and the correct culture (including compound forms like sr-Cyrl), and embed resources with the same data.*.res naming the old al-based task produced.

Fixes #82

Scope note

In #122 (comment), @NightOwl888 sketched three pieces for fixing cross-platform satellite builds:

  1. Switch from al to a csc-based MSBuild pipeline — matching what the .NET SDK does in CoreGenerateSatelliteAssemblies.
  2. Restructure satellite generation into its own MSBuild project that launches a helper per culture and also owns the ICU4J jar download/unpack.
  3. Testing strategy overhaul — a test-only NuGet package that can swap satellite assemblies for raw resource files, tests-as-source against packaged code, and a move to GitHub Actions for more parallelism.

This PR does #1 only. That's the narrow fix that unblocks non-Windows builds today: the satellite target now uses the in-process Csc task (from Microsoft.Build.Tasks.CodeAnalysis) with WriteCodeFragment for the attribute stub — the same pattern the SDK uses. Verified on macOS and in a Linux Docker container; full test suite passes on net6.0/net8.0/net9.0 on macOS.

#2 and #3 are deliberately out of scope. Both are substantial refactors (a new project structure, a new test-packaging model, a CI migration) that are worth doing on their own merits and reviewed as their own changes, and neither is required to close #82. Keeping this PR small and focused makes it easier to review and ship; the larger plan can layer on top without conflict since the al-specific plumbing is already gone.

AI: Co-authored with Claude Code (Opus 4.7)

@paulirwin paulirwin added the notes:improvement An enhancement to an existing feature label Apr 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates ICU4N’s build pipeline to generate ICU4N resource-only satellite assemblies in a cross-platform way by switching from the al.exe/Mono al approach to the in-process MSBuild Csc task (mirroring the .NET SDK’s satellite generation pattern), removing the need for Windows SDK tooling or Mono on non-Windows.

Changes:

  • Replace the custom al-based LinkAssemblies task with a WriteCodeFragment + in-proc Csc-based MSBuild pipeline for neutral and per-culture satellites.
  • Add a small MSBuild recursion target (_BuildOneSatelliteAssembly) to build localized satellites per culture (parallelized).
  • Remove the repo-level .NET Framework SDK Fixup properties that were only needed for the old al pipeline.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/ICU4N/ICU4N.csproj Reworks satellite assembly generation to use WriteCodeFragment + Csc and MSBuild recursion instead of al tooling.
Directory.Build.props Removes TargetFrameworkSDKToolsDirectory fixup properties that supported the prior al/Mono pipeline.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ICU4N/ICU4N.csproj Outdated
Comment on lines +277 to +280
DependsOnTargets="_CollectSatelliteInputs"
Condition=" '$(_SatelliteCulture)' != '' and '$(TargetFramework)' == '$(SatelliteAssemblyTargetFramework)' "
Inputs="$(MSBuildAllProjects);$(ICU4JResourcesDirectory)/$(_SatelliteCulture)/*.*"
Outputs="$(ICU4NSatelliteAssemblyOutputDir)/$(_SatelliteCulture)/$(AssemblyName).resources.dll">

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_BuildOneSatelliteAssembly tries to be incremental via Inputs=.../*.*, but MSBuild does not expand wildcards in Inputs/Outputs when they come from a property string. As written, the input path with *.* won’t exist as a real file, so the target will be considered out-of-date and will rebuild every culture on every build. Define an item list for the culture resources and reference that item list in Inputs (e.g., @(_OneCultureResource)), so incremental checks use the actual file set.

Copilot uses AI. Check for mistakes.
Comment thread src/ICU4N/ICU4N.csproj Outdated

@NightOwl888 NightOwl888 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a comment yesterday, but I didn't follow through on finishing the "review" in GitHub. We definitely should address the incremental build gate that we had before. Maybe there is a way to get it to work with Inputs and Outputs that doesn't cause it to be an all or nothing proposition with generating satellite assemblies, but I know the original approach from the LinkAssemblies task worked pretty well. I am sure there is probably a way to turn that logic into a separate task that returns a boolean and then use it in a batch so it runs before every call to csc, one for each resource file.

@paulirwin paulirwin requested a review from NightOwl888 April 23, 2026 22:28
Comment thread src/ICU4N/ICU4N.csproj Outdated
Deterministic="true"
TargetType="Library"
UseSharedCompilation="true" />
</Target>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran some checks and there are many differences between the old output and the new. I cannot point to any specific line to tell you what is wrong, so I will summarize all of the differences here with detail in drill down sections.

1. Localized Satellite Assembly

Old way

✔️ Compiles with net40 target
✔️ Includes culture
✔️ Strong names the assembly with PublicKeyToken=efb17c8e4f0e291b
✔️ Assembly version is correct
✔️ Includes assembly attributes for Title, Description, Company, Product, InformationalVersion, Copyright, FileVersion, and Version

  • All of these come from the root Directory.Targets.props file except for the 3 version attributes which come from Nerdbank.GitVersioning
Details
using System.Reflection;

// Assembly ICU4N.resources, Version=60.0.0.0, Culture=af, PublicKeyToken=efb17c8e4f0e291b
// MVID: FA9FF38F-D866-45AB-84B8-67F2065144AB
// Assembly references:
// mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

[assembly: AssemblyTitle("ICU4N")]
[assembly: AssemblyDescription("ICU (International Components for Unicode) is a set of libraries providing Unicode and Globalization support for software applications. It provides Text-boundary analysis (RuleBasedBreakIterator) as well as easy access to all of the many Unicode character properties, Unicode Normalization, Case Folding and other fundamental operations as specified by the Unicode Standard. ICU4N is a .NET port of ICU4J.")]
[assembly: AssemblyCompany("ICU4N")]
[assembly: AssemblyProduct("ICU4N")]
[assembly: AssemblyInformationalVersion("60.1.0-alpha.443+1d36d9c978")]
[assembly: AssemblyCopyright("Copyright © 2019 - 2026 ICU4N")]
[assembly: AssemblyFileVersion("60.1.0")]
[assembly: AssemblyVersion("60.0.0.0")]

New way

❌ Compiles with netstandard2.0 (not likely to load under net40)
✔️ Includes culture
✔️ Strong names the assembly with PublicKeyToken=efb17c8e4f0e291b
✔️ Assembly version is correct
❌ Doesn't include any version or package author attributes except AssemblyVersion
👉 Includes CompilationRelaxations, RuntimeCompatibility, and Debuggable attributes (probably no harm in these, but wouldn't hurt to check)

Details
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;

// Assembly ICU4N.resources, Version=60.0.0.0, Culture=af, PublicKeyToken=efb17c8e4f0e291b
// MVID: 01459FD1-3937-40A7-9245-1C169CCEF272
// Assembly references:
// netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyVersion("60.0.0.0")]

Note that .NET Core contains backward compatibility to load legacy assemblies, but net40 contains no support for loading netstandard2.0. But, the .NET SDK can compile net40 targets just fine, so this should be simple to fix.

For performance reasons, we don't build all satellite assemblies twice, we simply copy them into 2 different NuGet packages, ICU4N.Resources and ICU4N.Resources.NetFramework4.0.

2. Neutral Satellite Assembly

Old way

✔️ Compiles with net40 target
✔️ Culture is neutral
✔️ Strong names the assembly with PublicKeyToken=efb17c8e4f0e291b
✔️ Assembly version is correct
✔️ Includes assembly attributes for Title, Description, Company, Product, InformationalVersion, Copyright, FileVersion, and Version

  • All of these come from the root Directory.Targets.props file except for the 3 version attributes which come from Nerdbank.GitVersioning
Details
using System.Reflection;

// Assembly ICU4N.resources, Version=60.0.0.0, Culture=neutral, PublicKeyToken=efb17c8e4f0e291b
// MVID: 01F0572E-27A1-4FAC-A9F9-DD8503A6B45F
// Assembly references:
// mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

[assembly: AssemblyTitle("ICU4N")]
[assembly: AssemblyDescription("ICU (International Components for Unicode) is a set of libraries providing Unicode and Globalization support for software applications. It provides Text-boundary analysis (RuleBasedBreakIterator) as well as easy access to all of the many Unicode character properties, Unicode Normalization, Case Folding and other fundamental operations as specified by the Unicode Standard. ICU4N is a .NET port of ICU4J.")]
[assembly: AssemblyCompany("ICU4N")]
[assembly: AssemblyProduct("ICU4N")]
[assembly: AssemblyInformationalVersion("60.1.0-alpha.443+1d36d9c978")]
[assembly: AssemblyCopyright("Copyright © 2019 - 2026 ICU4N")]
[assembly: AssemblyFileVersion("60.1.0")]
[assembly: AssemblyVersion("60.0.0.0")]

New way

❌ Compiles with netstandard2.0 (not likely to load under net40)
✔️ Culture is neutral
✔️ Strong names the assembly with PublicKeyToken=efb17c8e4f0e291b
✔️ Assembly version is correct
❌ Doesn't include any version or package author attributes except AssemblyVersion
👉 Includes CompilationRelaxations, RuntimeCompatibility, and Debuggable attributes (probably no harm in these, but wouldn't hurt to check)

Details
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;

// Assembly ICU4N.resources, Version=60.0.0.0, Culture=neutral, PublicKeyToken=efb17c8e4f0e291b
// MVID: 5B27F173-4CDB-4480-89C4-FE22FBEE6051
// Assembly references:
// netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyVersion("60.0.0.0")]

3. Build

Old way

✔️ Builds all satellite assemblies if missing when running Build command
✔️ Builds all satellite assemblies if missing when running Rebuild command
✔️ Builds one missing satellite assembly when running Build command
✔️ Builds one missing satellite assembly when running Rebuild command
🚀 Takes about 2 minutes to build all satellite assemblies on my machine
✔️ Reports each localized satellite assembly written in build output with minimal output
✔️ Unpacks resources from the .jar if the _artifacts/icu4j-temp folder is missing
✔️ Downloads icu4j-60.1.jar if missing

Details
Build started at 10:34 PM...
1>------ Build started: Project: ICU4JResourceConverter, Configuration: Debug Any CPU ------
1>  ICU4JResourceConverter -> F:\Projects\ICU4N\src\tools\ICU4JResourceConverter\bin\Debug\net9.0\ICU4JResourceConverter.dll
2>------ Build started: Project: ICU4N, Configuration: Debug Any CPU ------
2>  SkipT4Transform: false
2>  UseMicrosoftT4: true
2>  Calling Transform...
2>  ResourcesRequireUpdate: False
2>  Generating Satellite Assembly for
2>  Generating Satellite Assembly for af
2>  Generating Satellite Assembly for agq
2>  Generating Satellite Assembly for ak
2>  Generating Satellite Assembly for am
2>  Generating Satellite Assembly for ar
2>  Generating Satellite Assembly for ars
2>  Generating Satellite Assembly for as
2>  Generating Satellite Assembly for asa
2>  Generating Satellite Assembly for ast
2>  Generating Satellite Assembly for az
2>  Generating Satellite Assembly for az-Cyrl
2>  Generating Satellite Assembly for az-Latn
2>  Generating Satellite Assembly for bas
2>  Generating Satellite Assembly for be
2>  Generating Satellite Assembly for bem
2>  Generating Satellite Assembly for bez
2>  Generating Satellite Assembly for bg
2>  Generating Satellite Assembly for bm
2>  Generating Satellite Assembly for bn
2>  Generating Satellite Assembly for bo
2>  Generating Satellite Assembly for br
2>  Generating Satellite Assembly for brx
2>  Generating Satellite Assembly for bs
2>  Generating Satellite Assembly for bs-Cyrl
2>  Generating Satellite Assembly for bs-Latn
2>  Generating Satellite Assembly for ca
2>  Generating Satellite Assembly for ccp
2>  Generating Satellite Assembly for ce
2>  Generating Satellite Assembly for cgg
2>  Generating Satellite Assembly for chr
2>  Generating Satellite Assembly for cs
2>  Generating Satellite Assembly for cy
2>  Generating Satellite Assembly for da
2>  Generating Satellite Assembly for dav
2>  Generating Satellite Assembly for de
2>  Generating Satellite Assembly for dje
2>  Generating Satellite Assembly for dsb
2>  Generating Satellite Assembly for dua
2>  Generating Satellite Assembly for dyo
2>  Generating Satellite Assembly for dz
2>  Generating Satellite Assembly for ebu
2>  Generating Satellite Assembly for ee
2>  Generating Satellite Assembly for el
2>  Generating Satellite Assembly for en
2>  Generating Satellite Assembly for eo
2>  Generating Satellite Assembly for es
2>  Generating Satellite Assembly for et
2>  Generating Satellite Assembly for eu
2>  Generating Satellite Assembly for ewo
2>  Generating Satellite Assembly for fa
2>  Generating Satellite Assembly for ff
2>  Generating Satellite Assembly for fi
2>  Generating Satellite Assembly for fil
2>  Generating Satellite Assembly for fo
2>  Generating Satellite Assembly for fr
2>  Generating Satellite Assembly for fur
2>  Generating Satellite Assembly for fy
2>  Generating Satellite Assembly for ga
2>  Generating Satellite Assembly for gd
2>  Generating Satellite Assembly for gl
2>  Generating Satellite Assembly for gsw
2>  Generating Satellite Assembly for gu
2>  Generating Satellite Assembly for guz
2>  Generating Satellite Assembly for gv
2>  Generating Satellite Assembly for ha
2>  Generating Satellite Assembly for haw
2>  Generating Satellite Assembly for he
2>  Generating Satellite Assembly for hi
2>  Generating Satellite Assembly for hr
2>  Generating Satellite Assembly for hsb
2>  Generating Satellite Assembly for hu
2>  Generating Satellite Assembly for hy
2>  Generating Satellite Assembly for id
2>  Generating Satellite Assembly for ig
2>  Generating Satellite Assembly for ii
2>  Generating Satellite Assembly for in
2>  Generating Satellite Assembly for is
2>  Generating Satellite Assembly for it
2>  Generating Satellite Assembly for iw
2>  Generating Satellite Assembly for ja
2>  Generating Satellite Assembly for jgo
2>  Generating Satellite Assembly for jmc
2>  Generating Satellite Assembly for ka
2>  Generating Satellite Assembly for kab
2>  Generating Satellite Assembly for kam
2>  Generating Satellite Assembly for kde
2>  Generating Satellite Assembly for kea
2>  Generating Satellite Assembly for khq
2>  Generating Satellite Assembly for ki
2>  Generating Satellite Assembly for kk
2>  Generating Satellite Assembly for kkj
2>  Generating Satellite Assembly for kl
2>  Generating Satellite Assembly for kln
2>  Generating Satellite Assembly for km
2>  Generating Satellite Assembly for kn
2>  Generating Satellite Assembly for ko
2>  Generating Satellite Assembly for kok
2>  Generating Satellite Assembly for ks
2>  Generating Satellite Assembly for ksb
2>  Generating Satellite Assembly for ksf
2>  Generating Satellite Assembly for ksh
2>  Generating Satellite Assembly for ku
2>  Generating Satellite Assembly for kw
2>  Generating Satellite Assembly for ky
2>  Generating Satellite Assembly for lag
2>  Generating Satellite Assembly for lb
2>  Generating Satellite Assembly for lg
2>  Generating Satellite Assembly for lkt
2>  Generating Satellite Assembly for ln
2>  Generating Satellite Assembly for lo
2>  Generating Satellite Assembly for lrc
2>  Generating Satellite Assembly for lt
2>  Generating Satellite Assembly for lu
2>  Generating Satellite Assembly for luo
2>  Generating Satellite Assembly for luy
2>  Generating Satellite Assembly for lv
2>  Generating Satellite Assembly for mas
2>  Generating Satellite Assembly for mer
2>  Generating Satellite Assembly for mfe
2>  Generating Satellite Assembly for mg
2>  Generating Satellite Assembly for mgh
2>  Generating Satellite Assembly for mgo
2>  Generating Satellite Assembly for mk
2>  Generating Satellite Assembly for ml
2>  Generating Satellite Assembly for mn
2>  Generating Satellite Assembly for mo
2>  Generating Satellite Assembly for mr
2>  Generating Satellite Assembly for ms
2>  Generating Satellite Assembly for mt
2>  Generating Satellite Assembly for mua
2>  Generating Satellite Assembly for my
2>  Generating Satellite Assembly for mzn
2>  Generating Satellite Assembly for naq
2>  Generating Satellite Assembly for nb
2>  Generating Satellite Assembly for nd
2>  Generating Satellite Assembly for nds
2>  Generating Satellite Assembly for ne
2>  Generating Satellite Assembly for nl
2>  Generating Satellite Assembly for nmg
2>  Generating Satellite Assembly for nn
2>  Generating Satellite Assembly for nnh
2>  Generating Satellite Assembly for no
2>  Generating Satellite Assembly for nus
2>  Generating Satellite Assembly for nyn
2>  Generating Satellite Assembly for om
2>  Generating Satellite Assembly for or
2>  Generating Satellite Assembly for os
2>  Generating Satellite Assembly for pa
2>  Generating Satellite Assembly for pa-Arab
2>  Generating Satellite Assembly for pa-Guru
2>  Generating Satellite Assembly for pl
2>  Generating Satellite Assembly for ps
2>  Generating Satellite Assembly for pt
2>  Generating Satellite Assembly for quz
2>  Generating Satellite Assembly for rm
2>  Generating Satellite Assembly for rn
2>  Generating Satellite Assembly for ro
2>  Generating Satellite Assembly for rof
2>  Generating Satellite Assembly for ru
2>  Generating Satellite Assembly for rw
2>  Generating Satellite Assembly for rwk
2>  Generating Satellite Assembly for sah
2>  Generating Satellite Assembly for saq
2>  Generating Satellite Assembly for sbp
2>  Generating Satellite Assembly for se
2>  Generating Satellite Assembly for seh
2>  Generating Satellite Assembly for ses
2>  Generating Satellite Assembly for sg
2>  Generating Satellite Assembly for sh
2>  Generating Satellite Assembly for shi
2>  Generating Satellite Assembly for shi-Latn
2>  Generating Satellite Assembly for shi-Tfng
2>  Generating Satellite Assembly for si
2>  Generating Satellite Assembly for sk
2>  Generating Satellite Assembly for sl
2>  Generating Satellite Assembly for smn
2>  Generating Satellite Assembly for sn
2>  Generating Satellite Assembly for so
2>  Generating Satellite Assembly for sq
2>  Generating Satellite Assembly for sr
2>  Generating Satellite Assembly for sr-Cyrl
2>  Generating Satellite Assembly for sr-Latn
2>  Generating Satellite Assembly for sv
2>  Generating Satellite Assembly for sw
2>  Generating Satellite Assembly for ta
2>  Generating Satellite Assembly for te
2>  Generating Satellite Assembly for teo
2>  Generating Satellite Assembly for tg
2>  Generating Satellite Assembly for th
2>  Generating Satellite Assembly for ti
2>  Generating Satellite Assembly for tl
2>  Generating Satellite Assembly for to
2>  Generating Satellite Assembly for tr
2>  Generating Satellite Assembly for tt
2>  Generating Satellite Assembly for twq
2>  Generating Satellite Assembly for tzm
2>  Generating Satellite Assembly for ug
2>  Generating Satellite Assembly for uk
2>  Generating Satellite Assembly for ur
2>  Generating Satellite Assembly for uz
2>  Generating Satellite Assembly for uz-Arab
2>  Generating Satellite Assembly for uz-Cyrl
2>  Generating Satellite Assembly for uz-Latn
2>  Generating Satellite Assembly for vai
2>  Generating Satellite Assembly for vai-Latn
2>  Generating Satellite Assembly for vai-Vaii
2>  Generating Satellite Assembly for vi
2>  Generating Satellite Assembly for vun
2>  Generating Satellite Assembly for wae
2>  Generating Satellite Assembly for wo
2>  Generating Satellite Assembly for xog
2>  Generating Satellite Assembly for yav
2>  Generating Satellite Assembly for yi
2>  Generating Satellite Assembly for yo
2>  Generating Satellite Assembly for zgh
2>  Generating Satellite Assembly for zh
2>  Generating Satellite Assembly for zh-Hans
2>  Generating Satellite Assembly for zh-Hant
2>  Generating Satellite Assembly for zu
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net40\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net451\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net462\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net8.0\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net9.0\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\netstandard2.0\ICU4N.dll
========== Build: 2 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
========== Build completed at 10:36 PM and took 02:06.053 minutes ==========

New way

❌ Doesn't build all satellite assemblies if missing when running Build command
✔️ Builds all satellite assemblies if missing when running Rebuild command
❌ Doesn't build one missing satellite assembly when running Build command
✔️ Builds one missing satellite assembly when running Rebuild command
👉 Takes nearly 5 minutes to build all satellite assemblies on my machine
❌ Reports no progress on satellite assembly builds with minimal build output
✔️ Unpacks resources from the .jar if the _artifacts/icu4j-temp folder is missing
✔️ Downloads icu4j-60.1.jar if missing

While the satellite assembly takes awhile, the incremental build ensures that debugging is fast. The most annoying part is there is no progress reported when the satellite assemblies are being output, so it looks like it is frozen.

Before, we bypassed the caching on Visual Studio to run the process during build. Basically, this means we could get lots of failures until we stumble upon trying the Rebuild command to try getting out of the scenario. Rebuild is what happens in CI so it will always work there.

Certainly, all of the correctness issues take priority over the speed, but it would be nice if it at least didn't force a rebuild just to restore satellite assemblies if they are missing.

Details
Rebuild started at 10:23 PM...
Restored F:\Projects\ICU4N\src\tools\ICU4JResourceConverter\ICU4JResourceConverter.csproj (in 58 ms).
Restored F:\Projects\ICU4N\src\ICU4N.Resources.NETFramework4.0\ICU4N.Resources.NETFramework4.0.csproj (in 76 ms).
Restored F:\Projects\ICU4N\src\ICU4N.Resources\ICU4N.Resources.csproj (in 74 ms).
Restored F:\Projects\ICU4N\tests\ICU4N.Tests.Transliterator\ICU4N.Tests.Transliterator.csproj (in 198 ms).
Restored F:\Projects\ICU4N\tests\ICU4N.Tests\ICU4N.Tests.csproj (in 197 ms).
Restored F:\Projects\ICU4N\tests\ICU4N.Tests.Collation\ICU4N.Tests.Collation.csproj (in 197 ms).
1>------ Rebuild All started: Project: ICU4JResourceConverter, Configuration: Debug Any CPU ------
Restored F:\Projects\ICU4N\src\ICU4N.CurrencyData\ICU4N.CurrencyData.csproj (in 151 ms).
Restored F:\Projects\ICU4N\src\ICU4N\ICU4N.csproj (in 236 ms).
Restored F:\Projects\ICU4N\src\ICU4N.LanguageData\ICU4N.LanguageData.csproj (in 237 ms).
Restored F:\Projects\ICU4N\src\ICU4N.RegionData\ICU4N.RegionData.csproj (in 237 ms).
Restored F:\Projects\ICU4N\src\ICU4N.TestFramework\ICU4N.TestFramework.csproj (in 240 ms).
Restored F:\Projects\ICU4N\src\ICU4N.Collation\ICU4N.Collation.csproj (in 237 ms).
Restored F:\Projects\ICU4N\src\ICU4N.Transliterator\ICU4N.Transliterator.csproj (in 222 ms).
1>  ICU4JResourceConverter -> F:\Projects\ICU4N\src\tools\ICU4JResourceConverter\bin\Debug\net9.0\ICU4JResourceConverter.dll
2>------ Rebuild All started: Project: ICU4N, Configuration: Debug Any CPU ------
2>  SkipT4Transform: false
2>  UseMicrosoftT4: true
2>  Calling Transform...
2>  ResourcesRequireUpdate: False
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net40\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net9.0\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net462\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net8.0\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\net451\ICU4N.dll
2>  ICU4N -> F:\Projects\ICU4N\src\ICU4N\bin\Debug\netstandard2.0\ICU4N.dll
========== Rebuild All: 2 succeeded, 0 failed, 0 skipped ==========
========== Rebuild completed at 10:28 PM and took 04:54.743 minutes ==========

paulirwin added a commit to paulirwin/ICU4N that referenced this pull request Apr 24, 2026
Addresses review feedback on NightOwl888#123:

- Satellites now reference mscorlib 4.0 (from
  Microsoft.NETFramework.ReferenceAssemblies.net40, which the main project
  already restores) instead of netstandard 2.0, so they load under .NET
  Framework 4.0. Csc is invoked with NoStandardLib=true to prevent
  netstandard.dll from being re-injected.

- Full set of package attributes (Title, Description, Company, Product,
  Copyright, InformationalVersion, FileVersion) are now written into every
  satellite via WriteCodeFragment, matching the old al.exe /template output.
  The version attributes depend on GetBuildVersion so Nerdbank.GitVersioning
  values are available.

- Fresh `dotnet build` now deploys satellites to bin/ and propagates them
  to dependent projects. The old <None Include="…/*.resources.dll"
  CopyToOutputDirectory=…> glob was evaluated before satellite generation
  ran, so on a clean build it captured zero files. Items are now injected
  into _NoneWithTargetPath right before _GetCopyToOutputDirectoryItemsFrom…
  so both local copy and transitive project-reference propagation pick
  them up.

- Added per-culture "Generating Satellite Assembly for <culture>" messages
  at high importance so minimal-verbosity VS output shows progress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NightOwl888

Copy link
Copy Markdown
Owner

FYI - the error:

##[error]src\ICU4N\ICU4N.csproj(244,11): Error MSB4057: The target "GetBuildVersion" does not exist in the project.

Usually is due to Nerdbank.GitVersioning and some build execution ordering problem. Although, I saw it happen in our custom analyzer projects, also.

paulirwin added a commit to paulirwin/ICU4N that referenced this pull request Apr 24, 2026
Addresses two regressions introduced in 393a63c (build #395 on PR NightOwl888#123):

- CI (Azure DevOps) failed with MSB4057: "target GetBuildVersion does
  not exist". Nerdbank.GitVersioning is conditionally referenced in
  Directory.Build.targets and excluded when TF_BUILD=true, since the
  pipeline passes /p:AssemblyVersion, /p:FileVersion and
  /p:InformationalVersion directly. A static DependsOnTargets reference
  to GetBuildVersion therefore fails to resolve on CI. Gate the
  dependency on the same condition NB.GV itself uses, so the target
  chain runs locally (where NB.GV populates InformationalVersion from
  git state) but is skipped on CI (where the props are already set by
  the pipeline inputs).

- Full solution build failed locally on .NET SDK 10 with MSB4006:
  "circular dependency in the target dependency graph involving target
  GetCopyToOutputDirectoryItems". The sub-invocation that collects
  transitive copy items from a ProjectReference pulls
  _IncludeGeneratedSatelliteAssemblies into the same subgraph as
  GetCopyToOutputDirectoryItems; combining DependsOnTargets=
  "GenerateOurSatelliteAssemblies" with BeforeTargets=
  "_GetCopyToOutputDirectoryItemsFromThisProject" closes a loop via the
  GenerateSatelliteAssemblies → ExecICU4JResourceConverter →
  GenerateOurSatelliteAssemblies chain. Drop the DependsOnTargets:
  main-build ordering already guarantees the satellite files exist
  before PrepareForRun → CopyFilesToOutputDirectory →
  GetCopyToOutputDirectoryItems runs, and for transitive queries the
  referenced project's Build has completed before the consumer asks
  for its copy items.

Verified locally on macOS (SDK 10.0.201):
- Full solution build: succeeds.
- Full test suite (net6.0/net8.0/net9.0): all 4462 pass, 50 skipped
  (net4x TFMs skipped — require Mono, unrelated environment limit).
- 220 satellites (1 neutral + 219 localized) propagate into consuming
  test projects' bin/ directories.
- Simulated CI build (TF_BUILD=true, no NB.GV, /p: version inputs):
  succeeds and produces all 220 satellites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Owl888#82)

Generate the ICU4N resource-only satellite assemblies with the
in-process `Csc` MSBuild task (from Microsoft.Build.Tasks.CodeAnalysis,
shipped with every modern dotnet SDK) — the same pattern the .NET SDK
itself uses in Microsoft.NET.Sdk.targets::CoreGenerateSatelliteAssemblies.
This replaces the prior `al.exe` / Mono `al` pipeline, which required
the Windows SDK or Mono to build the netstandard2.0 target. Build now
runs on Windows, macOS, and Linux with no extra tooling.

Details:

- csc has no -culture: or -version: value flags (those exist on `al`).
  WriteCodeFragment emits a per-culture .cs stub containing
  [assembly: AssemblyVersion] and [assembly: AssemblyCulture] which is
  passed to Csc as a source input. AssemblyCulture drives the
  AssemblyDef row's Culture field, which is what the CLR satellite
  resolver reads.

- @(ReferencePath) is filtered down to the BCL anchors the attributes
  live on (mscorlib / netstandard / System.Runtime) — same filter the
  SDK's satellite target applies.

- MSBuild's target- and task-level batching don't compose cleanly with
  WriteCodeFragment (which treats any metadata key as a named attribute
  argument). The escape hatch is to <MSBuild>-recurse into a helper
  target once per culture, passing the culture as a global property.
  Inside the recursion everything is scalar and Csc keeps sharing its
  compilation server across all 219 calls.

- Removes the TargetFrameworkSDKToolsDirectory fixup in
  Directory.Build.props (was used to locate `al`; no longer needed).

Verified: 220 satellites (1 neutral + 219 localized) build on macOS
(SDK 10.0.201) and in Linux Docker (SDK 9.0.313), are strong-name
signed with the ICU4N public key token, stamp version 60.0.0.0 and the
correct culture (including compound forms like sr-Cyrl), and embed
resources with the same data.*.res naming the old al-based task
produced. Full test suites (ICU4N.Tests, ICU4N.Tests.Collation,
ICU4N.Tests.Transliterator) pass on macOS for net6.0, net8.0, net9.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

notes:improvement An enhancement to an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix cross-platform satellite assembly generation

3 participants