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
1 change: 1 addition & 0 deletions README.fork.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ If no version is specified, it defaults to `11.9.1`. You can also pass a build c

The build produces the following NuGet packages in `forked_nugets/`:

- `Avalonia.<version>.nupkg` (repackaged official meta-package with patched Avalonia.Base.dll)
- `Avalonia.Android.<version>.nupkg`
- `Avalonia.iOS.<version>.nupkg`
- `Avalonia.OpenGL.<version>.nupkg`
Expand Down
158 changes: 146 additions & 12 deletions build-fork-nugets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,135 @@ echo "============================================"
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"

# Pack order: OpenGL/Skia first (no fork-to-fork deps), then Android/iOS
# Step 1: Build Avalonia.Base first (we need the patched DLLs)
echo ">> Building Avalonia.Base ..."
dotnet pack "$SCRIPT_DIR/src/Avalonia.Base/Avalonia.Base.csproj" \
-c "$CONFIGURATION" \
-p:ForkVersion="$FORK_VERSION" \
-o "$OUTPUT_DIR"
echo ""

# Step 2: Repackage the official Avalonia meta-package with our patched Avalonia.Base.dll.
#
# The official Avalonia nupkg bundles Avalonia.Base.dll (via Numerge).
# We replace those DLLs with our patched build and re-version the package
# so consumers get the fix without conflicting copies.
NUGET_CACHE="$HOME/.nuget/packages"
OFFICIAL_NUPKG="$NUGET_CACHE/avalonia/$BASE_VERSION/avalonia.$BASE_VERSION.nupkg"

if [ ! -f "$OFFICIAL_NUPKG" ]; then
echo ">> Official Avalonia $BASE_VERSION not found in NuGet cache. Downloading..."
mkdir -p "$NUGET_CACHE/avalonia/$BASE_VERSION"
curl -sSL -o "$OFFICIAL_NUPKG" \
"https://api.nuget.org/v3-flatcontainer/avalonia/$BASE_VERSION/avalonia.$BASE_VERSION.nupkg"
if [ ! -f "$OFFICIAL_NUPKG" ]; then
echo " ERROR: Failed to download Avalonia $BASE_VERSION from nuget.org"
exit 1
fi
echo " Downloaded to $OFFICIAL_NUPKG"
fi

echo ">> Repackaging official Avalonia $BASE_VERSION with patched Avalonia.Base DLLs..."

# Built DLLs from step 1
BASE_BIN="$SCRIPT_DIR/src/Avalonia.Base/bin/$CONFIGURATION"

python3 - "$OFFICIAL_NUPKG" "$BASE_BIN" "$OUTPUT_DIR" "$BASE_VERSION" "$FORK_VERSION" <<'PYEOF'
import sys, os, zipfile, tempfile, shutil
import xml.etree.ElementTree as ET

official_nupkg = sys.argv[1]
base_bin = sys.argv[2]
output_dir = sys.argv[3]
base_version = sys.argv[4]
fork_version = sys.argv[5]

# Map TFM folders in the nupkg to our build output directories
TFM_MAP = {
"net8.0": "net8.0",
"net6.0": "net6.0",
"netstandard2.0": "netstandard2.0",
}

with tempfile.TemporaryDirectory() as tmpdir:
# Extract official package
with zipfile.ZipFile(official_nupkg, 'r') as z:
z.extractall(tmpdir)

# Replace Avalonia.Base.dll in lib/ and ref/ for each TFM
for nupkg_tfm, build_tfm in TFM_MAP.items():
built_dll = os.path.join(base_bin, build_tfm, "Avalonia.Base.dll")
built_xml = os.path.join(base_bin, build_tfm, "Avalonia.Base.xml")

if not os.path.exists(built_dll):
print(f" WARNING: {built_dll} not found, skipping {nupkg_tfm}")
continue

for prefix in ["lib", "ref"]:
target_dll = os.path.join(tmpdir, prefix, nupkg_tfm, "Avalonia.Base.dll")
target_xml = os.path.join(tmpdir, prefix, nupkg_tfm, "Avalonia.Base.xml")

if os.path.exists(target_dll):
shutil.copy2(built_dll, target_dll)
print(f" Replaced {prefix}/{nupkg_tfm}/Avalonia.Base.dll")

if os.path.exists(target_xml) and os.path.exists(built_xml):
shutil.copy2(built_xml, target_xml)
print(f" Replaced {prefix}/{nupkg_tfm}/Avalonia.Base.xml")

# Update version in nuspec
nuspec_files = [f for f in os.listdir(tmpdir) if f.endswith(".nuspec")]
if nuspec_files:
nuspec_path = os.path.join(tmpdir, nuspec_files[0])
tree = ET.parse(nuspec_path)
root = tree.getroot()

# Find namespace
ns = ""
for uri in [
"http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd",
"http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd",
"http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd",
]:
if root.find(f"{{{uri}}}metadata") is not None:
ns = uri
break

if ns:
ET.register_namespace("", ns)
metadata = root.find(f"{{{ns}}}metadata")
version_el = metadata.find(f"{{{ns}}}version")
if version_el is not None:
version_el.text = fork_version
print(f" Version: {base_version} -> {fork_version}")

tree.write(nuspec_path, xml_declaration=True, encoding="utf-8")

# Remove [Content_Types].xml signature files that NuGet will regenerate
for f in [".signature.p7s"]:
p = os.path.join(tmpdir, "package", f)
if os.path.exists(p):
os.remove(p)

# Repackage
out_nupkg = os.path.join(output_dir, f"Avalonia.{fork_version}.nupkg")
with zipfile.ZipFile(out_nupkg, 'w', zipfile.ZIP_DEFLATED) as zout:
for dirpath, dirnames, filenames in os.walk(tmpdir):
for fn in filenames:
full = os.path.join(dirpath, fn)
arcname = os.path.relpath(full, tmpdir)
zout.write(full, arcname)
print(f" Created {os.path.basename(out_nupkg)}")
PYEOF

echo ""

# Remove the standalone Avalonia.Base nupkg — it's now inside the Avalonia meta-package
rm -f "$OUTPUT_DIR"/Avalonia.Base.*.nupkg
echo ">> Removed standalone Avalonia.Base nupkg (now bundled in Avalonia meta-package)"
echo ""

# Step 3: Pack the remaining forked projects
PROJECTS=(
"src/Avalonia.OpenGL/Avalonia.OpenGL.csproj"
"src/Skia/Avalonia.Skia/Avalonia.Skia.csproj"
Expand All @@ -37,14 +165,14 @@ done
#
# The official Avalonia build uses Numerge to merge sub-packages into the
# Avalonia meta-package. These sub-packages don't exist on nuget.org:
# Avalonia.Base, Avalonia.Controls, Avalonia.DesignerSupport, Avalonia.OpenGL,
# Avalonia.Controls, Avalonia.DesignerSupport, Avalonia.OpenGL,
# Avalonia.Metal, Avalonia.Vulkan, Avalonia.Dialogs, Avalonia.Markup,
# Avalonia.Markup.Xaml, Avalonia.MicroCom
#
# Our dotnet pack creates deps on these non-existent packages. We fix this by:
# 1. Removing deps on merged/bundled packages
# 2. Ensuring a dep on Avalonia >= base_version exists (it provides those DLLs)
# 3. Pinning Avalonia dep to base_version (not fork_version)
# 1. Removing deps on merged/bundled packages (they're inside the Avalonia meta-package)
# 2. Rewriting Avalonia deps to point to our fork version (since we now ship a forked Avalonia nupkg)
# 3. Removing deps on Avalonia.Base (now bundled in our forked Avalonia meta-package)

echo ">> Post-processing nupkgs..."

Expand Down Expand Up @@ -82,6 +210,11 @@ def find_ns(root):
for fname in sorted(os.listdir(output_dir)):
if not fname.endswith(".nupkg"):
continue
# Skip the repackaged Avalonia meta-package — already handled
# Its filename is "Avalonia.<fork_version>.nupkg" (no sub-name like .Android, .Skia, etc.)
pkg_name = fname.rsplit("." + fork_version, 1)[0] if ("." + fork_version) in fname else fname
if pkg_name == "Avalonia":
continue
nupkg_path = os.path.join(output_dir, fname)

with tempfile.TemporaryDirectory() as tmpdir:
Expand Down Expand Up @@ -118,11 +251,11 @@ for fname in sorted(os.listdir(output_dir)):

if dep_id == "Avalonia":
has_avalonia_dep = True
# Pin to base version (official meta-package on nuget.org)
if dep_ver == fork_version:
dep.set("version", base_version)
# Point to our forked Avalonia meta-package
if dep_ver != fork_version:
dep.set("version", fork_version)
modified = True
print(f" {pkg_id}: Avalonia dep {fork_version} -> {base_version}")
print(f" {pkg_id}: Avalonia dep {dep_ver} -> {fork_version}")

elif dep_id in MERGED:
# Remove deps on packages bundled in the Avalonia meta-package
Expand All @@ -144,10 +277,10 @@ for fname in sorted(os.listdir(output_dir)):
if needs_avalonia_dep and not has_avalonia_dep:
new_dep = ET.SubElement(group, f"{{{ns}}}dependency")
new_dep.set("id", "Avalonia")
new_dep.set("version", base_version)
new_dep.set("version", fork_version)
new_dep.set("exclude", "Build,Analyzers")
modified = True
print(f" {pkg_id}: added dep on Avalonia {base_version}")
print(f" {pkg_id}: added dep on Avalonia {fork_version}")

if modified:
tree.write(nuspec_path, xml_declaration=True, encoding="utf-8")
Expand All @@ -169,4 +302,5 @@ ls -1 "$OUTPUT_DIR"/*.nupkg 2>/dev/null
echo ""
echo "Copy to your local feed and clean stale cache:"
echo " cp $OUTPUT_DIR/*.nupkg /opt/dev/local_nugets/"
echo " rm -rf ~/.nuget/packages/avalonia*/11.9.1"
echo " rm -rf ~/.nuget/packages/avalonia/$FORK_VERSION"
echo " rm -rf ~/.nuget/packages/avalonia.*/$FORK_VERSION"
3 changes: 2 additions & 1 deletion build/ForkOverride.targets
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<Project>
<!--
When ForkVersion is passed (e.g. dotnet pack -p:ForkVersion=11.9.1),
only the 4 forked projects get the custom version.
only the forked projects get the custom version.
All other projects keep their original version from SharedVersion.props,
so ProjectReference-based dependency entries point to the base version.
A post-processing step then fixes deps on packages that don't exist on
nuget.org (they're bundled in the Avalonia meta-package by Numerge).
-->
<PropertyGroup Condition="'$(ForkVersion)' != ''">
<_IsForkProject Condition="'$(MSBuildProjectName)' == 'Avalonia.Base'">true</_IsForkProject>
<_IsForkProject Condition="'$(MSBuildProjectName)' == 'Avalonia.Skia'">true</_IsForkProject>
<_IsForkProject Condition="'$(MSBuildProjectName)' == 'Avalonia.OpenGL'">true</_IsForkProject>
<_IsForkProject Condition="'$(MSBuildProjectName)' == 'Avalonia.Android'">true</_IsForkProject>
Expand Down
Loading