Browse Source

Add Linux and macOS publish and packaging scripts

With the move to Avalonia the app runs on Linux and macOS, so the
distribution tooling grows beyond Windows: publish.ps1 gains a
-Platform parameter (the default keeps the Windows behavior unchanged),
and new BuildTools scripts package the self-contained publish outputs
into a zip, deb, and rpm on Linux and a zipped ILSpy.app on macOS.

Nothing is signed at package time; signing happens offline on the
release manager machine. Info-ZIP zip is used instead of
Compress-Archive because the latter drops unix mode bits. deb/rpm are
built with dpkg-deb/rpmbuild directly (both preinstalled on GitHub
ubuntu runners) following sourcegit-scm/sourcegit's proven model,
including the libicu OR-chain in the deb control file and disabled
auto-requires for the self-contained rpm payload. Pre-release versions
map '-' to '~' so both package managers sort them before the release.
The macOS bundle's Info.plist now carries placeholders stamped at
package time; on Windows the spec/control/desktop files are kept LF via
.gitattributes because Linux tools consume them verbatim.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3404/head
Christoph Wille 4 weeks ago
parent
commit
b6b1bf99be
  1. 5
      .gitattributes
  2. 119
      BuildTools/package-linux.ps1
  3. 63
      BuildTools/package-macos.ps1
  4. 12
      BuildTools/packaging/linux/deb/control.template
  5. 9
      BuildTools/packaging/linux/ilspy.desktop
  6. BIN
      BuildTools/packaging/linux/ilspy.png
  7. 36
      BuildTools/packaging/linux/rpm/ilspy.spec
  8. 7
      ILSpy/Assets/macos/Info.plist
  9. 40
      publish.ps1

5
.gitattributes vendored

@ -2,3 +2,8 @@ @@ -2,3 +2,8 @@
*.cs text eol=crlf diff=csharp
*.sln text eol=crlf
*.csproj text eol=crlf
# Consumed verbatim by Linux tools (shell-executed rpm spec, dpkg control, desktop entry);
# CRLF breaks them, so keep them LF even in Windows working trees.
*.spec text eol=lf
*.desktop text eol=lf
control.template text eol=lf

119
BuildTools/package-linux.ps1

@ -0,0 +1,119 @@ @@ -0,0 +1,119 @@
#!/usr/bin/env pwsh
# Package the linux-x64 self-contained publish output (created by `./publish.ps1 -Platform linux`)
# into the Linux distribution artifacts:
# ILSpy_linux-x64_<version>.zip Info-ZIP archive (preserves the execute flag, unlike
# Compress-Archive/7z, which drop unix mode bits)
# ilspy_<version>-1_amd64.deb built with dpkg-deb
# ilspy-<version>-1.x86_64.rpm built with rpmbuild
# Must run on Linux. zip and dpkg-deb are preinstalled on GitHub's ubuntu runners;
# locally: sudo apt-get install -y zip dpkg rpm
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release',
# Defaults to the CI-computed version (env var, then the ILSPY_VERSION file written by
# BuildTools/ghactions-install.ps1), falling back to the committed VERSION file for
# local runs.
[string]$Version,
[string]$StagingDirectory = 'buildartifacts'
)
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$repoRoot = Split-Path -Parent $PSScriptRoot
$resources = Join-Path $PSScriptRoot 'packaging/linux'
foreach ($tool in 'zip', 'dpkg-deb', 'rpmbuild') {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
throw "'$tool' not found. This script must run on Linux; install the tools with: sudo apt-get install -y zip dpkg rpm"
}
}
if (-not $Version) {
if ($env:ILSPY_VERSION_NUMBER) {
$Version = $env:ILSPY_VERSION_NUMBER
}
elseif (Test-Path (Join-Path $repoRoot 'ILSPY_VERSION')) {
$Version = (Get-Content (Join-Path $repoRoot 'ILSPY_VERSION') -Raw).Trim()
}
else {
$Version = (Get-Content (Join-Path $repoRoot 'VERSION') -Raw).Trim()
}
}
$publishDir = Join-Path $repoRoot "ILSpy/bin/$Configuration/net10.0/linux-x64/publish/selfcontained"
if (-not (Test-Path (Join-Path $publishDir 'ILSpy'))) {
throw "Publish output not found at $publishDir. Run ./publish.ps1 -Configuration $Configuration -Platform linux first."
}
$staging = New-Item -ItemType Directory -Force (Join-Path $repoRoot $StagingDirectory)
# ----- zip -----
$zipPath = Join-Path $staging "ILSpy_linux-x64_$Version.zip"
Remove-Item $zipPath -ErrorAction Ignore
Push-Location $publishDir
try {
zip -r -q $zipPath . -x '*.pdb'
}
finally {
Pop-Location
}
Write-Host "created $zipPath"
# Both dpkg and rpm reject '-' inside a version (it separates the package revision in deb
# and is illegal in the rpm Version tag). '~' is legal in both and sorts *before* the
# plain version, which is the correct semantic for pre-release suffixes like '-preview1'.
$pkgVersion = $Version -replace '-', '~'
# Shared payload layout for both package formats: the app in /opt/ilspy, a launcher
# symlink on PATH, and desktop integration files.
function Copy-PackagePayload([string]$root) {
New-Item -ItemType Directory -Force (Join-Path $root 'opt/ilspy'), (Join-Path $root 'usr/bin'),
(Join-Path $root 'usr/share/applications'), (Join-Path $root 'usr/share/icons/hicolor/256x256/apps') | Out-Null
Copy-Item "$publishDir/*" (Join-Path $root 'opt/ilspy') -Recurse
Get-ChildItem (Join-Path $root 'opt/ilspy') -Filter '*.pdb' -Recurse | Remove-Item
chmod 755 (Join-Path $root 'opt/ilspy/ILSpy')
ln -s /opt/ilspy/ILSpy (Join-Path $root 'usr/bin/ilspy')
Copy-Item (Join-Path $resources 'ilspy.desktop') (Join-Path $root 'usr/share/applications')
Copy-Item (Join-Path $resources 'ilspy.png') (Join-Path $root 'usr/share/icons/hicolor/256x256/apps')
chmod 644 (Join-Path $root 'usr/share/applications/ilspy.desktop') (Join-Path $root 'usr/share/icons/hicolor/256x256/apps/ilspy.png')
}
# ----- deb -----
$debRoot = Join-Path ([IO.Path]::GetTempPath()) 'ilspy-deb'
Remove-Item -Recurse -Force $debRoot -ErrorAction Ignore
New-Item -ItemType Directory -Force (Join-Path $debRoot 'DEBIAN') | Out-Null
Copy-PackagePayload $debRoot
# Debian has no unversioned libicu package and the soname-versioned name differs per
# release, so depend on an OR-chain covering the releases we care about (the leading
# plain 'libicu' catches derivatives that do ship a virtual package).
$icuVersions = 78, 77, 76, 74, 72, 71, 70, 69, 68, 67, 66, 65, 63
$icuDeps = (@('libicu') + ($icuVersions | ForEach-Object { "libicu$_" })) -join ' | '
$installedSize = [int]((Get-ChildItem $debRoot -Recurse -File | Measure-Object Length -Sum).Sum / 1024)
$control = (Get-Content (Join-Path $resources 'deb/control.template') -Raw).
Replace('@VERSION@', "$pkgVersion-1").
Replace('@ARCH@', 'amd64').
Replace('@INSTALLED_SIZE@', "$installedSize").
Replace('@ICU_DEPS@', $icuDeps)
Set-Content (Join-Path $debRoot 'DEBIAN/control') $control
$debPath = Join-Path $staging "ilspy_${pkgVersion}-1_amd64.deb"
# gzip instead of dpkg's zstd default so the package installs on older dpkg versions
dpkg-deb -Zgzip --root-owner-group --build $debRoot $debPath
Write-Host "created $debPath"
# ----- rpm -----
$rpmTop = Join-Path ([IO.Path]::GetTempPath()) 'ilspy-rpm'
Remove-Item -Recurse -Force $rpmTop -ErrorAction Ignore
$rpmRoot = Join-Path $rpmTop 'payload'
New-Item -ItemType Directory -Force $rpmRoot | Out-Null
Copy-PackagePayload $rpmRoot
rpmbuild -bb (Join-Path $resources 'rpm/ilspy.spec') --target x86_64 `
--define "_topdir $rpmTop" `
--define "_version $pkgVersion" `
--define "_payload_dir $rpmRoot"
$rpm = Get-ChildItem (Join-Path $rpmTop 'RPMS') -Filter '*.rpm' -Recurse
Move-Item $rpm.FullName $staging -Force
Write-Host "created $(Join-Path $staging $rpm.Name)"

63
BuildTools/package-macos.ps1

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
#!/usr/bin/env pwsh
# Package the osx-arm64 ILSpy.app bundle (created by `./publish.ps1 -Platform macos` via the
# BuildMacAppBundle target in ILSpy.csproj) into ILSpy_macos-arm64_<version>.zip.
# Stamps the version placeholders in the bundle's Info.plist first.
# Must run on macOS/Linux: Info-ZIP's zip preserves the execute flag and (-y) symlinks,
# which Compress-Archive does not.
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release',
# Defaults to the CI-computed version (env var, then the ILSPY_VERSION file written by
# BuildTools/ghactions-install.ps1), falling back to the committed VERSION file for
# local runs.
[string]$Version,
[string]$StagingDirectory = 'buildartifacts'
)
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$repoRoot = Split-Path -Parent $PSScriptRoot
if (-not (Get-Command zip -ErrorAction SilentlyContinue)) {
throw "'zip' not found. This script needs Info-ZIP, available on macOS and Linux."
}
if (-not $Version) {
if ($env:ILSPY_VERSION_NUMBER) {
$Version = $env:ILSPY_VERSION_NUMBER
}
elseif (Test-Path (Join-Path $repoRoot 'ILSPY_VERSION')) {
$Version = (Get-Content (Join-Path $repoRoot 'ILSPY_VERSION') -Raw).Trim()
}
else {
$Version = (Get-Content (Join-Path $repoRoot 'VERSION') -Raw).Trim()
}
}
$publishDir = Join-Path $repoRoot "ILSpy/bin/$Configuration/net10.0/osx-arm64/publish"
$appDir = Join-Path $publishDir 'ILSpy.app'
if (-not (Test-Path $appDir)) {
throw "ILSpy.app not found at $appDir. Run ./publish.ps1 -Configuration $Configuration -Platform macos first."
}
# CFBundleVersion / CFBundleShortVersionString must be dotted numerics; strip the
# pre-release suffix (11.0.0.8948-preview1 -> 11.0.0.8948) and shorten to three parts.
$numericVersion = ($Version -split '-')[0]
$shortVersion = ($numericVersion -split '\.')[0..2] -join '.'
$plistPath = Join-Path $appDir 'Contents/Info.plist'
$plist = (Get-Content $plistPath -Raw).
Replace('ILSPY_SHORT_VERSION', $shortVersion).
Replace('ILSPY_VERSION', $numericVersion)
Set-Content $plistPath $plist
$staging = New-Item -ItemType Directory -Force (Join-Path $repoRoot $StagingDirectory)
$zipPath = Join-Path $staging "ILSpy_macos-arm64_$Version.zip"
Remove-Item $zipPath -ErrorAction Ignore
Push-Location $publishDir
try {
zip -r -y -q $zipPath ILSpy.app -x '*.pdb'
}
finally {
Pop-Location
}
Write-Host "created $zipPath"

12
BuildTools/packaging/linux/deb/control.template

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
Package: ilspy
Version: @VERSION@
Priority: optional
Section: devel
Architecture: @ARCH@
Installed-Size: @INSTALLED_SIZE@
Depends: libx11-6, libice6, libsm6, libfontconfig1, @ICU_DEPS@, xdg-utils
Maintainer: ICSharpCode Team <https://github.com/icsharpcode>
Homepage: https://github.com/icsharpcode/ILSpy
Description: .NET assembly browser and decompiler
ILSpy is the open-source .NET assembly browser and decompiler,
built on Avalonia and the ICSharpCode.Decompiler engine.

9
BuildTools/packaging/linux/ilspy.desktop

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
[Desktop Entry]
Name=ILSpy
Comment=.NET assembly browser and decompiler
Exec=/opt/ilspy/ILSpy %F
Icon=ilspy
Terminal=false
Type=Application
Categories=Development;
MimeType=application/x-msdownload;application/vnd.microsoft.portable-executable;

BIN
BuildTools/packaging/linux/ilspy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

36
BuildTools/packaging/linux/rpm/ilspy.spec

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
# Built by BuildTools/package-linux.ps1, which stages the complete payload tree
# (/opt/ilspy + launcher symlink + desktop integration) and passes it in via
# rpmbuild -bb ilspy.spec --target x86_64
# --define "_topdir <tmp>" --define "_version <version>" --define "_payload_dir <staged tree>"
# The payload is a prebuilt self-contained .NET publish; skip post-install processing
# (stripping the bundled native libraries breaks them) and debuginfo extraction.
%global __os_install_post %{nil}
%global debug_package %{nil}
%global _build_id_links none
Name: ilspy
Version: %{_version}
Release: 1
Summary: .NET assembly browser and decompiler
License: MIT
URL: https://github.com/icsharpcode/ILSpy
# The self-contained payload bundles its own .NET runtime; automatic dependency scanning
# would generate requires for every bundled shared object, so dependencies are listed
# manually instead.
AutoReqProv: no
Requires: libX11, libSM, libICE, fontconfig, libicu
%description
ILSpy is the open-source .NET assembly browser and decompiler,
built on Avalonia and the ICSharpCode.Decompiler engine.
%install
mkdir -p %{buildroot}
cp -a %{_payload_dir}/. %{buildroot}/
%files
/opt/ilspy
/usr/bin/ilspy
/usr/share/applications/ilspy.desktop
/usr/share/icons/hicolor/256x256/apps/ilspy.png

7
ILSpy/Assets/macos/Info.plist

@ -8,10 +8,13 @@ @@ -8,10 +8,13 @@
<string>ILSpy</string>
<key>CFBundleIdentifier</key>
<string>net.sharpdevelop.ilspy</string>
<!-- Version placeholders are stamped by BuildTools/package-macos.ps1 at package time.
A bare `dotnet publish -r osx-arm64` leaves them in place, which is harmless for
local development bundles. -->
<key>CFBundleVersion</key>
<string>10.1.0</string>
<string>ILSPY_VERSION</string>
<key>CFBundleShortVersionString</key>
<string>10.1</string>
<string>ILSPY_SHORT_VERSION</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>

40
publish.ps1

@ -1,21 +1,28 @@ @@ -1,21 +1,28 @@
#!/usr/bin/env pwsh
# Publish the Windows distribution bundles. Invoked by the build workflow as:
# .\publish.ps1 --configuration <Debug|Release>
# The win-x64 framework-dependent bundle is produced for every configuration (the binaries zip).
# The win-arm64 framework-dependent and win-x64 self-contained bundles are Release-only.
# Publish the per-platform distribution bundles. Invoked by the build workflow as:
# ./publish.ps1 -Configuration <Debug|Release> -Platform <windows|linux|macos>
# windows: win-x64 framework-dependent bundle for every configuration (the binaries zip);
# the win-arm64 framework-dependent and win-x64 self-contained bundles are Release-only.
# linux: linux-x64 self-contained bundle.
# macos: osx-arm64 self-contained bundle; the BuildMacAppBundle target in ILSpy.csproj
# assembles ILSpy.app next to the publish directory.
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release'
[string]$Configuration = 'Release',
[ValidateSet('windows', 'linux', 'macos')]
[string]$Platform = 'windows'
)
$ErrorActionPreference = 'Stop'
$base = "./ILSpy/bin/$Configuration/net10.0"
$output_x64 = "$base/win-x64/publish/fwdependent"
dotnet publish ./ILSpy/ILSpy.csproj -c $Configuration --no-restore --no-self-contained -r win-x64 -o $output_x64
dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c $Configuration --no-restore --no-self-contained -r win-x64 -o $output_x64
switch ($Platform) {
'windows' {
$output_x64 = "$base/win-x64/publish/fwdependent"
dotnet publish ./ILSpy/ILSpy.csproj -c $Configuration --no-restore --no-self-contained -r win-x64 -o $output_x64
dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c $Configuration --no-restore --no-self-contained -r win-x64 -o $output_x64
if ($Configuration -eq 'Release') {
if ($Configuration -eq 'Release') {
$output_arm64 = "$base/win-arm64/publish/fwdependent"
dotnet publish ./ILSpy/ILSpy.csproj -c $Configuration --no-restore --no-self-contained -r win-arm64 -o $output_arm64
dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c $Configuration --no-restore --no-self-contained -r win-arm64 -o $output_arm64
@ -23,4 +30,19 @@ if ($Configuration -eq 'Release') { @@ -23,4 +30,19 @@ if ($Configuration -eq 'Release') {
$output_x64_selfcontained = "$base/win-x64/publish/selfcontained"
dotnet publish ./ILSpy/ILSpy.csproj -c $Configuration --no-restore --self-contained -r win-x64 -o $output_x64_selfcontained
dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c $Configuration --no-restore --self-contained -r win-x64 -o $output_x64_selfcontained
}
}
'linux' {
$output = "$base/linux-x64/publish/selfcontained"
dotnet publish ./ILSpy/ILSpy.csproj -c $Configuration --no-restore --self-contained -r linux-x64 -o $output
dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c $Configuration --no-restore --self-contained -r linux-x64 -o $output
}
'macos' {
$output = "$base/osx-arm64/publish/selfcontained"
# The ReadyToRun plugin must be published first: BuildMacAppBundle runs after the
# ILSpy publish and snapshots the publish directory into ILSpy.app/Contents/MacOS,
# so everything that belongs in the bundle has to be in place by then.
dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c $Configuration --no-restore --self-contained -r osx-arm64 -o $output
dotnet publish ./ILSpy/ILSpy.csproj -c $Configuration --no-restore --self-contained -r osx-arm64 -o $output
}
}

Loading…
Cancel
Save