diff --git a/.gitattributes b/.gitattributes index 4945948f6..a6227fffc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/BuildTools/package-linux.ps1 b/BuildTools/package-linux.ps1 new file mode 100644 index 000000000..07409ba3a --- /dev/null +++ b/BuildTools/package-linux.ps1 @@ -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_.zip Info-ZIP archive (preserves the execute flag, unlike +# Compress-Archive/7z, which drop unix mode bits) +# ilspy_-1_amd64.deb built with dpkg-deb +# ilspy--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)" diff --git a/BuildTools/package-macos.ps1 b/BuildTools/package-macos.ps1 new file mode 100644 index 000000000..aaf907e73 --- /dev/null +++ b/BuildTools/package-macos.ps1 @@ -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_.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" diff --git a/BuildTools/packaging/linux/deb/control.template b/BuildTools/packaging/linux/deb/control.template new file mode 100644 index 000000000..56e8d9655 --- /dev/null +++ b/BuildTools/packaging/linux/deb/control.template @@ -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 +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. diff --git a/BuildTools/packaging/linux/ilspy.desktop b/BuildTools/packaging/linux/ilspy.desktop new file mode 100644 index 000000000..ef15d5128 --- /dev/null +++ b/BuildTools/packaging/linux/ilspy.desktop @@ -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; diff --git a/BuildTools/packaging/linux/ilspy.png b/BuildTools/packaging/linux/ilspy.png new file mode 100644 index 000000000..0f746e9fa Binary files /dev/null and b/BuildTools/packaging/linux/ilspy.png differ diff --git a/BuildTools/packaging/linux/rpm/ilspy.spec b/BuildTools/packaging/linux/rpm/ilspy.spec new file mode 100644 index 000000000..96242e18b --- /dev/null +++ b/BuildTools/packaging/linux/rpm/ilspy.spec @@ -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 " --define "_version " --define "_payload_dir " + +# 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 diff --git a/ILSpy/Assets/macos/Info.plist b/ILSpy/Assets/macos/Info.plist index ec4f16566..37b4a269c 100644 --- a/ILSpy/Assets/macos/Info.plist +++ b/ILSpy/Assets/macos/Info.plist @@ -8,10 +8,13 @@ ILSpy CFBundleIdentifier net.sharpdevelop.ilspy + CFBundleVersion - 10.1.0 + ILSPY_VERSION CFBundleShortVersionString - 10.1 + ILSPY_SHORT_VERSION CFBundlePackageType APPL CFBundleSignature diff --git a/publish.ps1 b/publish.ps1 index 873cebd27..a885ee196 100644 --- a/publish.ps1 +++ b/publish.ps1 @@ -1,26 +1,48 @@ #!/usr/bin/env pwsh -# Publish the Windows distribution bundles. Invoked by the build workflow as: -# .\publish.ps1 --configuration -# 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 -Platform +# 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') { - $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 + 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 - $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 + $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 + } }