From f9aa8b969077a90ae19ab0220ef94f88745fb3a7 Mon Sep 17 00:00:00 2001 From: Andi Idogawa Date: Wed, 30 Sep 2026 09:56:31 +0200 Subject: [PATCH] Binary releases: self-contained archives for Linux, Windows and macOS Util/package-release.sh publishes a self-contained ReadyToRun ir and packs it with the standard library and the launchers in the checkout's layout; release.yml runs it on each platform and publishes the archives when a v* tag is pushed. ir.sh and ir.cmd run the Release build when there is no Debug one, which is what an unpacked archive has. irubyc no longer references or copies the .NET runtime that a self-contained ir carries next to it (CS0433 on every type), so it works from a release too. --- .github/workflows/release.yml | 154 ++++++++++++++++++ README.md | 24 ++- Src/StdLib/ironruby/ironruby/compiler.rb | 21 +++ .../ironruby/ironruby/compiler/library.rb | 2 + Util/package-release.sh | 67 ++++++++ ir.cmd | 3 + ir.sh | 10 +- 7 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100755 Util/package-release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1b312ae7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,154 @@ +name: Release + +# Binary builds: a self-contained IronRuby for each platform, made by +# Util/package-release.sh. Pushing a tag v* publishes them as a GitHub release +# (a pre-release while the version has a suffix such as -preview1); +# running the workflow by hand, or a pull request that changes the packaging, only +# keeps them as workflow artifacts. +on: + push: + tags: ['v*'] + pull_request: + paths: + - .github/workflows/release.yml + - Util/package-release.sh + workflow_dispatch: + +permissions: + contents: read + +env: + DOTNET_NOLOGO: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + # Unlike CI, which follows prism's main branch, a release is built from the prism + # revision Src/Prism/Generated was generated from (Src/Prism/README.md), the one the + # vendored Ruby half of prism in Src/StdLib belongs to. + PRISM_REF: c53699f7f20f6b6aa4704216fbfecec897fe419a + +jobs: + package: + name: ${{ matrix.rid }} + runs-on: ${{ matrix.os }} + # macOS is untested (README, Platforms): a failure there does not hold the release back. + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, rid: linux-x64, experimental: false } + - { os: windows-latest, rid: win-x64, experimental: false } + - { os: macos-latest, rid: osx-arm64, experimental: true } + defaults: + run: + shell: bash + steps: + # See ci.yml: the sources must reach the parser byte for byte. + - name: Keep checkouts byte-identical on Windows + if: runner.os == 'Windows' + run: git config --global core.autocrlf input + + - uses: actions/checkout@v4 + with: + path: ironruby + + - uses: actions/checkout@v4 + with: + repository: IronLanguages/dlr + path: dlr + + - uses: actions/checkout@v4 + with: + repository: ruby/prism + ref: ${{ env.PRISM_REF }} + path: prism + + - name: Patch the DLR + run: | + for patch in ironruby/Build/dlr-patches/*.patch; do + echo "applying $patch" + git -C dlr apply "../$patch" + done + + # The DLR multi-targets up to net9.0, so the build needs the .NET 10 SDK; the + # release itself targets net8.0. + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + + # Built as in ci.yml; see there for why Windows links the sources directly. + - name: Build libprism + working-directory: prism + run: | + ruby templates/template.rb + if [ "$RUNNER_OS" = Windows ]; then + command -v gcc >/dev/null || export PATH="/c/msys64/ucrt64/bin:/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH" + mkdir -p build + shopt -s nullglob + sources=(src/*.c src/*/*.c) + gcc -shared -O2 -std=c99 -DPRISM_EXPORT_SYMBOLS -Iinclude \ + -o build/libprism.dll "${sources[@]}" + else + make shared + fi + ls -l build + + # The committed loader must be the one this prism revision generates; a mismatch + # would build fine and then misread the syntax tree. + - name: Check the prism loader against the prism revision + working-directory: ironruby/Src/Prism + run: | + ruby generate.rb ../../../prism/config.yml "$RUNNER_TEMP/prism-gen" + diff -r --strip-trailing-cr Generated "$RUNNER_TEMP/prism-gen" + + - name: Package + working-directory: ironruby + run: | + if [ "$GITHUB_REF_TYPE" = tag ]; then + export IR_VERSION="${GITHUB_REF_NAME#v}" + else + version=$(sed -n 's/.*DisplayVersion = "\(.*\)";.*/\1/p' Src/Ruby/CurrentVersion.cs) + export IR_VERSION="$version+$(git rev-parse --short HEAD)" + fi + Util/package-release.sh "${{ matrix.rid }}" "$GITHUB_WORKSPACE/dist" + + - uses: actions/upload-artifact@v4 + with: + name: ironruby-${{ matrix.rid }} + path: | + dist/*.tar.gz + dist/*.zip + if-no-files-found: error + + release: + name: GitHub release + if: github.ref_type == 'tag' + needs: package + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Checksums + working-directory: dist + run: sha256sum * | tee SHA256SUMS + + - name: Publish + env: + GH_TOKEN: ${{ github.token }} + run: | + prerelease="" + case "$GITHUB_REF_NAME" in *-*) prerelease="--prerelease" ;; esac + gh release create "$GITHUB_REF_NAME" dist/* \ + --repo "$GITHUB_REPOSITORY" --title "IronRuby ${GITHUB_REF_NAME#v}" \ + --generate-notes $prerelease diff --git a/README.md b/README.md index 7356838d..c7de23ea 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,28 @@ Linux is the primary target and the one the numbers above are measured on. **Win readline are not implemented on Windows; `IO.console` is nil, so irb falls back to its ANSI input path. macOS is untested. +## Binary releases + +[Releases](https://github.com/Largo/ironruby/releases) carry a ready-to-run IronRuby for +Linux x64, Windows x64 and macOS arm64 (untested). They are self-contained, so no .NET has +to be installed. Unpack one and run the launchers in it: + +```console +$ tar xzf ironruby--linux-x64.tar.gz && cd ironruby--linux-x64 +$ ./ir.sh -e 'puts RUBY_DESCRIPTION' +$ ./irb.sh +$ ./igem.sh install rack +``` + +On Windows, unzip it and use `ir.cmd`, `irb.cmd` and `igem.cmd`. The archive has the same +layout as a checkout, with the Release build in `Src/Console/bin/Release/net8.0`, so +everything above applies to it unchanged. `irubyc` works too, but it still needs the +.NET SDK to compile the application. + +`Util/package-release.sh ` builds such an archive from a checkout, and +`.github/workflows/release.yml` builds all three and publishes them when a `v*` tag is +pushed. + ## Building Requires the .NET SDK, plus Ruby and a C compiler to build prism. @@ -460,5 +482,5 @@ table, so such a redirection is refused with `EINVAL`), and the Unix-only half o ## License -Apache License 2.0, as the original. See `Public/License.html`. +Apache License 2.0, as the original. See [`Src/Public`](Src/Public/LICENSE.APACHE.html). The original content description is preserved in [`README.txt`](README.txt). diff --git a/Src/StdLib/ironruby/ironruby/compiler.rb b/Src/StdLib/ironruby/ironruby/compiler.rb index 16801f49..3fce0e36 100644 --- a/Src/StdLib/ironruby/ironruby/compiler.rb +++ b/Src/StdLib/ironruby/ironruby/compiler.rb @@ -26,6 +26,7 @@ require "optparse" require "fileutils" require "rbconfig" +require "json" require_relative "compiler/library" module IronRuby @@ -324,11 +325,29 @@ def check_single_file_rid(rid) end def managed_assemblies(bin) + runtime = runtime_pack_files(bin) Dir.glob(File.join(bin, "*.dll")).map { |f| File.basename(f, ".dll") } .reject { |n| n == "ir" || n.start_with?("ir.") } # the console host itself + .reject { |n| runtime.include?(n + ".dll") } .select { |n| managed_assembly?(File.join(bin, n + ".dll")) }.sort end + # A self-contained ir (a binary release) has the whole .NET runtime next to it: + # System.Private.CoreLib, libcoreclr and ~180 more. Referencing those from the + # generated host fails to compile (CS0433, every type twice), and the app gets its + # runtime from the SDK anyway, so they are neither referenced nor copied. ir.deps.json + # lists them under the runtime pack; a framework-dependent build has no such entry. + def runtime_pack_files(bin) + deps = JSON.parse(File.read(File.join(bin, "ir.deps.json"))) + deps["targets"].values.flat_map do |libraries| + libraries.select { |name, _| name.start_with?("runtimepack.") }.values + end.flat_map do |files| + (files["runtime"] || {}).keys + (files["native"] || {}).keys + end.map { |f| File.basename(f) } + rescue Errno::ENOENT, JSON::ParserError + [] + end + # A native DLL (libprism.dll from a Windows build, say) has no assembly name. def managed_assembly?(path) System::Reflection::AssemblyName.get_assembly_name(path) @@ -519,11 +538,13 @@ def run_dotnet(options, work, name, out) # yaml.rb) and libprism, which is a native library. def copy_runtime(options, bin, out, skip: []) FileUtils.mkdir_p(out) + runtime = runtime_pack_files(bin) Dir.glob(File.join(bin, "*")).each do |src| next unless File.file?(src) base = File.basename(src) next unless base =~ /\.(dll|so|dylib)\z/ next if base.start_with?("ir.") + next if runtime.include?(base) # A referenced assembly is already in the publish output; a second copy as # content would collide with it (NETSDK1152). next if skip.include?(File.basename(base, ".dll")) diff --git a/Src/StdLib/ironruby/ironruby/compiler/library.rb b/Src/StdLib/ironruby/ironruby/compiler/library.rb index 61738c64..53fbb63d 100644 --- a/Src/StdLib/ironruby/ironruby/compiler/library.rb +++ b/Src/StdLib/ironruby/ironruby/compiler/library.rb @@ -102,8 +102,10 @@ def stage_payload(payload, host_dll) # Native libraries: only this platform's. A Linux build directory can hold a # Windows libprism.dll as well; it would be dead weight here. native_ext = native_extension + runtime = Compiler.runtime_pack_files(@bin) Dir.glob(File.join(@bin, "*")).each do |src| next unless File.file?(src) && File.extname(src) == native_ext + next if runtime.include?(File.basename(src)) next if assemblies.include?(File.basename(src, ".dll")) next if native_ext == ".dll" && Compiler.managed_assembly?(src) say "native: #{File.basename(src)}" diff --git a/Util/package-release.sh b/Util/package-release.sh new file mode 100755 index 00000000..bf32cb70 --- /dev/null +++ b/Util/package-release.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Builds a binary release of IronRuby for one platform: +# +# Util/package-release.sh linux-x64 # -> dist/ironruby--linux-x64.tar.gz +# Util/package-release.sh win-x64 out # -> out/ironruby--win-x64.zip +# IR_VERSION=1.2.0-preview1 Util/package-release.sh osx-arm64 +# +# The archive keeps the source tree's layout - the launchers at the top, the build in +# Src/Console/bin/Release/, the standard library in Src/StdLib - so ir.sh/ir.cmd, +# igem, irb and irubyc work from the unpacked directory exactly as they do in a +# checkout (ir.sh picks the Release build when there is no Debug one). +# +# The build is self-contained, so it runs on a machine with no .NET installed, and +# ReadyToRun, so IronRuby's own assemblies start as native code (see publish-r2r.sh). +# irubyc still needs the .NET SDK, as it does from a checkout. +# +# It needs what a normal build needs: ../dlr with Build/dlr-patches applied, and +# ../prism/build holding the libprism for this platform. The RID must be this +# machine's: the archive carries this machine's libprism. +set -euo pipefail + +RID=${1:?usage: Util/package-release.sh RID [OUTDIR]} +IR_ROOT=$(cd "$(dirname "$0")/.." && pwd) +OUT=$(mkdir -p "${2:-dist}" && cd "${2:-dist}" && pwd) +: "${IR_TFM:=net8.0}" +VERSION=${IR_VERSION:-$(sed -n 's/.*DisplayVersion = "\(.*\)";.*/\1/p' "$IR_ROOT/Src/Ruby/CurrentVersion.cs")} +NAME="ironruby-$VERSION-$RID" +STAGE="$OUT/$NAME" + +rm -rf "$STAGE" +mkdir -p "$STAGE/Src" + +dotnet publish "$IR_ROOT/Src/Console/Ruby.Console.csproj" \ + -c Release -r "$RID" --self-contained true -f "$IR_TFM" \ + -p:IronRubyTargetFrameworks="$IR_TFM" -p:PublishReadyToRun=true \ + -o "$STAGE/Src/Console/bin/Release/$IR_TFM" +rm -f "$STAGE/Src/Console/bin/Release/$IR_TFM"/*.pdb + +case "$RID" in + win-*) launchers=(ir.cmd irb.cmd igem.cmd iirb.cmd irubyc.cmd) ;; + *) launchers=(ir.sh irb.sh igem.sh iirb.sh irubyc.sh) ;; +esac +for f in "${launchers[@]}" README.md; do + cp "$IR_ROOT/$f" "$STAGE/" +done +cp -R "$IR_ROOT/Src/StdLib" "$STAGE/Src/" +rm -f "$STAGE/Src/StdLib/StdLib.rbproj" +cp -R "$IR_ROOT/Src/Public" "$STAGE/License" + +# A quick check that the unpacked tree starts, before it is archived. +case "$RID" in + win-*) (cd "$STAGE" && cmd //c ir.cmd -e 'puts RUBY_DESCRIPTION') ;; + *) "$STAGE/ir.sh" -e 'puts RUBY_DESCRIPTION' ;; +esac + +cd "$OUT" +case "$RID" in + win-*) + rm -f "$NAME.zip" + if command -v 7z >/dev/null; then 7z a -tzip -bd "$NAME.zip" "$NAME" >/dev/null + else zip -qr "$NAME.zip" "$NAME"; fi + ARCHIVE="$NAME.zip" ;; + *) + tar -czf "$NAME.tar.gz" "$NAME" + ARCHIVE="$NAME.tar.gz" ;; +esac +echo "packaged: $OUT/$ARCHIVE" diff --git a/ir.cmd b/ir.cmd index a2f770d0..c4ec3258 100644 --- a/ir.cmd +++ b/ir.cmd @@ -11,6 +11,9 @@ if "%IR_ROOT:~-1%"=="\" set "IR_ROOT=%IR_ROOT:~0,-1%" rem IR_CONFIG=Release runs the optimized build instead. Debug stays the default, rem exactly as in ir.sh. +rem A binary release (Util/package-release.sh) ships only the Release build: when +rem IR_CONFIG is not set and there is no Debug build, run that one. +if "%IR_CONFIG%"=="" if not exist "%IR_ROOT%\Src\Console\bin\Debug\" if exist "%IR_ROOT%\Src\Console\bin\Release\" set "IR_CONFIG=Release" if "%IR_CONFIG%"=="" set "IR_CONFIG=Debug" rem IR_TFM picks the target framework out of the multi-targeted build (net8.0;net10.0), diff --git a/ir.sh b/ir.sh index 0f32a3c8..670bc711 100755 --- a/ir.sh +++ b/ir.sh @@ -24,7 +24,15 @@ S="$IR_ROOT/Src/StdLib" # too - and it passes the same specs and the same IronRuby.Tests. Util/run-tests.sh and # regen-initializers.sh read IR_CONFIG as well, so one export covers a whole session. # Debug stays the default because every worktree and every agent script builds it. -: "${IR_CONFIG:=Debug}" +# A binary release (Util/package-release.sh) ships only the Release build: when +# IR_CONFIG is not set and there is no Debug build, run that one. +if [ -z "$IR_CONFIG" ]; then + IR_CONFIG=Debug + if [ ! -x "$IR_ROOT/Src/Console/bin/Debug/${IR_TFM:-net8.0}/ir" ] && + [ -x "$IR_ROOT/Src/Console/bin/Release/${IR_TFM:-net8.0}/ir" ]; then + IR_CONFIG=Release + fi +fi # IR_TFM picks the target framework, the same way IR_CONFIG picks the configuration. The # tree multi-targets net8.0;net10.0 (Directory.Build.props), so one `dotnet build` leaves