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