diff --git a/.github/steps/install_dependencies/action.yml b/.github/steps/install_dependencies/action.yml index 7619b886..6d714383 100644 --- a/.github/steps/install_dependencies/action.yml +++ b/.github/steps/install_dependencies/action.yml @@ -26,4 +26,4 @@ runs: if: runner.os == 'Linux' && inputs.job-platform == 'linux' run: | sudo apt-get update - sudo apt-get install -y libgtk-3-0 + sudo apt-get install -y desktop-file-utils lintian libgtk-3-0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9776e6c..09d78150 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -452,24 +452,12 @@ jobs: path: ${{ github.workspace }}/Emerald-macOS-arm64-app.zip retention-days: 14 - # Snap packages are built natively on each architecture — Uno Platform does not - # support cross-compilation for snap targets as of the current SDK version. publish-linux: - name: Linux Snap (${{ matrix.arch }}) + name: Linux x64 Packages needs: ci-filter if: ${{ needs.ci-filter.outputs.run_linux == 'true' }} - runs-on: ${{ matrix.runner }} + runs-on: ubuntu-22.04 timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - include: - - arch: x64 - runner: ubuntu-22.04 - runtime: linux-x64 - - arch: arm64 - runner: ubuntu-22.04-arm - runtime: linux-arm64 steps: - name: Checkout code @@ -482,9 +470,108 @@ jobs: with: job-platform: linux + - name: Publish Linux x64 app and stage shared package files + shell: bash + env: + PACKAGE_VERSION: ${{ needs.ci-filter.outputs.package_version }} + PUBLIC_VERSION: ${{ needs.ci-filter.outputs.public_version }} + UPDATE_CHANNEL: ${{ needs.ci-filter.outputs.release_channel }} + RELEASE_TAG: ${{ needs.ci-filter.outputs.release_tag }} + COMMIT_SHA: ${{ github.sha }} + BUILD_TIMESTAMP_UTC: ${{ needs.ci-filter.outputs.build_timestamp_utc }} + run: scripts/linux/publish-linux-x64.sh + + - name: Build Linux Debian package + shell: bash + env: + PACKAGE_VERSION: ${{ needs.ci-filter.outputs.package_version }} + run: scripts/linux/build-deb.sh + + - name: Verify Linux AppDir staging + shell: bash + run: | + set -euo pipefail + + test -x "$GITHUB_WORKSPACE/AppDir/usr/lib/emerald/Emerald" + test -x "$GITHUB_WORKSPACE/AppDir/usr/bin/emerald" + test -f "$GITHUB_WORKSPACE/AppDir/usr/share/applications/emerald.desktop" + test -f "$GITHUB_WORKSPACE/AppDir/usr/share/applications/com.riversidevalley.Emerald.desktop" + test -f "$GITHUB_WORKSPACE/AppDir/usr/share/icons/hicolor/256x256/apps/emerald.png" + + - name: Build Linux AppImage + shell: bash + env: + EMERALD_PACKAGE_VERSION: ${{ needs.ci-filter.outputs.package_version }} + EMERALD_APPIMAGE_NAME: Emerald-linux-x86_64.AppImage + run: | + set -euo pipefail + + docker run --rm \ + -e "EMERALD_PACKAGE_VERSION=$EMERALD_PACKAGE_VERSION" \ + -e "EMERALD_APPIMAGE_NAME=$EMERALD_APPIMAGE_NAME" \ + -v "$GITHUB_WORKSPACE:/github/workspace" \ + -w /github/workspace \ + appimagecrafters/appimage-builder:1.1.0 \ + bash -lc 'apt-get update && apt-get install -y squashfs-tools && appimage-builder --recipe=scripts/linux/AppImageBuilder.x64.yml --skip-test' + + - name: Stage and validate Linux AppImage + shell: bash + run: | + set -euo pipefail + + APPIMAGE_FILE="$(find "$GITHUB_WORKSPACE" -maxdepth 1 -name '*.AppImage' -type f -print -quit)" + if [ -z "$APPIMAGE_FILE" ]; then + echo "No AppImage file found after appimage-builder completed" + exit 1 + fi + + STAGED="$GITHUB_WORKSPACE/artifacts/linux/final/Emerald-linux-x86_64.AppImage" + cp "$APPIMAGE_FILE" "$STAGED" + chmod +x "$STAGED" + "$STAGED" --appimage-extract >/dev/null + rm -rf squashfs-root + + - name: Prepare Linux Arch package recipe + shell: bash + env: + PACKAGE_VERSION: ${{ needs.ci-filter.outputs.package_version }} + run: scripts/linux/prepare-arch-package.sh + + - name: Build Linux Arch package + shell: bash + run: | + set -euo pipefail + + docker run --rm \ + -v "$GITHUB_WORKSPACE:/workspace" \ + -w /workspace/artifacts/linux/arch/pkgbuild \ + archlinux:base-devel \ + bash -lc ' + set -euo pipefail + useradd -m builder + chown -R builder:builder /workspace/artifacts/linux/arch + su builder -c "cd /workspace/artifacts/linux/arch/pkgbuild && makepkg --printsrcinfo > .SRCINFO && makepkg --packagelist && makepkg --nodeps --cleanbuild --force" + ' + + - name: Stage Linux Arch artifacts + shell: bash + run: | + set -euo pipefail + + PKGBUILD_DIR="$GITHUB_WORKSPACE/artifacts/linux/arch/pkgbuild" + FINAL_DIR="$GITHUB_WORKSPACE/artifacts/linux/final" + PKG_FILE="$(find "$PKGBUILD_DIR" -maxdepth 1 -name '*.pkg.tar.zst' -type f | head -n 1)" + if [ -z "$PKG_FILE" ]; then + echo "No Arch package file found after makepkg completed" + exit 1 + fi + + cp "$PKG_FILE" "$FINAL_DIR/emerald-${{ needs.ci-filter.outputs.package_version }}-1-x86_64.pkg.tar.zst" + tar -czf "$FINAL_DIR/emerald-aur-recipe.tar.gz" -C "$PKGBUILD_DIR" PKGBUILD .SRCINFO + # Snapcraft is installed after the base dependencies so that the snap daemon # is ready before we invoke dotnet publish with PackageFormat=snap. - # core22 matches the Ubuntu 22.04 runner base used by both matrix legs. + # core22 matches the Ubuntu 22.04 runner base. # --destructive-mode is required in CI because LXD/Multipass are unavailable # on single-use GitHub-hosted runners; it is safe here because the environment # is ephemeral and discarded after the job completes. @@ -497,16 +584,16 @@ jobs: sudo snap install snapcraft --classic sudo snap install multipass - - name: Publish Linux Snap (${{ matrix.arch }}) + - name: Publish Linux Snap shell: bash env: # Modern Snapcraft equivalent to --destructive-mode - SNAPCRAFT_BUILD_ENVIRONMENT: host + SNAPCRAFT_BUILD_ENVIRONMENT: host run: | dotnet publish ./Emerald/Emerald.csproj \ -c Release \ -f net10.0-desktop \ - -r "${{ matrix.runtime }}" \ + -r linux-x64 \ -p:SelfContained=true \ -p:PackageFormat=snap \ -p:UnoSnapBuildProvider=none \ @@ -530,31 +617,66 @@ jobs: run: | # Uno places the .snap in the publish output folder; find it regardless # of the exact versioned filename that snapcraft generates. - SNAP_FILE="$(find . -name '*.snap' -not -path '*/.git/*' -type f | head -n 1)" + SNAP_FILE="$(find "$GITHUB_WORKSPACE" \ + \( -path "$GITHUB_WORKSPACE/.git" \ + -o -path "$GITHUB_WORKSPACE/AppDir" \ + -o -path "$GITHUB_WORKSPACE/appimage-build" \) -prune \ + -o -name '*.snap' -type f -print -quit)" if [ -z "$SNAP_FILE" ]; then echo "No .snap file found after publish" exit 1 fi echo "Found snap: $SNAP_FILE" - STAGED="$GITHUB_WORKSPACE/Emerald-linux-${{ matrix.arch }}.snap" + STAGED="$GITHUB_WORKSPACE/artifacts/linux/final/Emerald-linux-x64.snap" cp "$SNAP_FILE" "$STAGED" echo "snap_path=$STAGED" >> "$GITHUB_OUTPUT" - - name: Upload CI Linux Snap artifact (${{ matrix.arch }}) + - name: Upload CI Linux Snap artifact uses: actions/upload-artifact@v4 with: - name: Emerald-linux-${{ matrix.arch }}-snap + name: Emerald-linux-x64-snap path: ${{ steps.locate.outputs.snap_path }} if-no-files-found: error retention-days: 14 - - name: Upload release Linux Snap transfer artifact (${{ matrix.arch }}) - if: ${{ needs.ci-filter.outputs.is_release_flow == 'true' }} + - name: Upload CI Linux tar artifact uses: actions/upload-artifact@v4 with: - name: Emerald-linux-${{ matrix.arch }}-ReleaseDraft-Snap - path: ${{ steps.locate.outputs.snap_path }} + name: Emerald-linux-x64-tar + path: ${{ github.workspace }}/artifacts/linux/final/Emerald-linux-x64.tar.gz + if-no-files-found: error + retention-days: 14 + + - name: Upload CI Linux AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: Emerald-linux-x64-appimage + path: ${{ github.workspace }}/artifacts/linux/final/Emerald-linux-x86_64.AppImage + if-no-files-found: error + retention-days: 14 + + - name: Upload CI Linux Debian artifact + uses: actions/upload-artifact@v4 + with: + name: Emerald-linux-x64-deb + path: ${{ github.workspace }}/artifacts/linux/final/emerald_${{ needs.ci-filter.outputs.package_version }}_amd64.deb + if-no-files-found: error + retention-days: 14 + + - name: Upload CI Linux Arch artifact + uses: actions/upload-artifact@v4 + with: + name: Emerald-linux-x64-arch + path: ${{ github.workspace }}/artifacts/linux/final/emerald-${{ needs.ci-filter.outputs.package_version }}-1-x86_64.pkg.tar.zst + if-no-files-found: error + retention-days: 14 + + - name: Upload CI Linux AUR recipe artifact + uses: actions/upload-artifact@v4 + with: + name: Emerald-linux-x64-aur-recipe + path: ${{ github.workspace }}/artifacts/linux/final/emerald-aur-recipe.tar.gz if-no-files-found: error retention-days: 14 @@ -588,13 +710,37 @@ jobs: - name: Download Linux x64 Snap artifact uses: actions/download-artifact@v4 with: - name: Emerald-linux-x64-ReleaseDraft-Snap + name: Emerald-linux-x64-snap + path: release-assets/linux + + - name: Download Linux x64 tar artifact + uses: actions/download-artifact@v4 + with: + name: Emerald-linux-x64-tar + path: release-assets/linux + + - name: Download Linux x64 AppImage artifact + uses: actions/download-artifact@v4 + with: + name: Emerald-linux-x64-appimage + path: release-assets/linux + + - name: Download Linux x64 Debian artifact + uses: actions/download-artifact@v4 + with: + name: Emerald-linux-x64-deb + path: release-assets/linux + + - name: Download Linux x64 Arch artifact + uses: actions/download-artifact@v4 + with: + name: Emerald-linux-x64-arch path: release-assets/linux - - name: Download Linux arm64 Snap artifact + - name: Download Linux x64 AUR recipe artifact uses: actions/download-artifact@v4 with: - name: Emerald-linux-arm64-ReleaseDraft-Snap + name: Emerald-linux-x64-aur-recipe path: release-assets/linux - name: Build release metadata files @@ -655,4 +801,4 @@ jobs: generate_release_notes: true files: ${{ steps.files.outputs.files }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/linux/AppImageBuilder.x64.yml b/scripts/linux/AppImageBuilder.x64.yml new file mode 100644 index 00000000..9aea9676 --- /dev/null +++ b/scripts/linux/AppImageBuilder.x64.yml @@ -0,0 +1,53 @@ +version: 1 + +AppDir: + path: ./AppDir + app_info: + id: com.riversidevalley.Emerald + name: Emerald + icon: emerald + version: !ENV ${EMERALD_PACKAGE_VERSION} + exec: usr/lib/emerald/Emerald + exec_args: $@ + runtime: + env: + APPDIR_LIBRARY_PATH: '$APPDIR/usr/lib/emerald:$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu' + GTK_THEME: Default + apt: + arch: amd64 + sources: + - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920D1991BC93C' + - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted universe multiverse + - sourceline: deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted universe multiverse + include: + - libgtk-3-0 + - libgtk-3-bin + - libgtk-3-common + - libx11-6 + - libfontconfig1 + - libfreetype6 + - libgcc-s1 + - libstdc++6 + - libcanberra-gtk3-module + exclude: + - hicolor-icon-theme + - humanity-icon-theme + files: + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + - usr/bin/kernel-install + - usr/lib/kernel + - usr/share/bug + - lib/systemd + - etc/init.d + +AppImage: + arch: x86_64 + file_name: !ENV ${EMERALD_APPIMAGE_NAME} + update-information: None + sign-key: None diff --git a/scripts/linux/build-deb.sh b/scripts/linux/build-deb.sh new file mode 100755 index 00000000..b1fcb2a2 --- /dev/null +++ b/scripts/linux/build-deb.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUTPUT_ROOT="${OUTPUT_ROOT:-$PWD/artifacts/linux}" +PACKAGE_VERSION="${PACKAGE_VERSION:?PACKAGE_VERSION is required}" +INSTALL_ROOT="$OUTPUT_ROOT/install-root" +DEB_ROOT="$OUTPUT_ROOT/deb/root" +FINAL_DIR="$OUTPUT_ROOT/final" +DEB_PATH="$FINAL_DIR/emerald_${PACKAGE_VERSION}_amd64.deb" + +if [ ! -d "$INSTALL_ROOT/opt/emerald" ]; then + echo "Install root was not found. Run scripts/linux/publish-linux-x64.sh first." + exit 1 +fi + +rm -rf "$DEB_ROOT" +mkdir -p "$DEB_ROOT/DEBIAN" "$FINAL_DIR" +cp -a "$INSTALL_ROOT/." "$DEB_ROOT/" + +INSTALLED_SIZE="$(du -sk "$DEB_ROOT/opt/emerald" "$DEB_ROOT/usr" | awk '{total += $1} END {print total}')" + +cat > "$DEB_ROOT/DEBIAN/control" < +Installed-Size: $INSTALLED_SIZE +Depends: libgtk-3-0, libx11-6, libfontconfig1, libfreetype6, libgcc-s1, libstdc++6, libc6 +Homepage: https://github.com/RiversideValley/Emerald +Description: Open-source cross-platform Minecraft launcher + Emerald is an open-source cross-platform Minecraft launcher made with .NET. +EOF + +find "$DEB_ROOT" -type d -exec chmod 755 {} + +chmod 644 "$DEB_ROOT/DEBIAN/control" +dpkg-deb --build --root-owner-group "$DEB_ROOT" "$DEB_PATH" +dpkg-deb --info "$DEB_PATH" + +if command -v lintian >/dev/null 2>&1; then + lintian "$DEB_PATH" || true +fi diff --git a/scripts/linux/prepare-arch-package.sh b/scripts/linux/prepare-arch-package.sh new file mode 100755 index 00000000..8e6b851e --- /dev/null +++ b/scripts/linux/prepare-arch-package.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUTPUT_ROOT="${OUTPUT_ROOT:-$PWD/artifacts/linux}" +PACKAGE_VERSION="${PACKAGE_VERSION:?PACKAGE_VERSION is required}" +INSTALL_ROOT="$OUTPUT_ROOT/install-root" +ARCH_ROOT="$OUTPUT_ROOT/arch" +PKGBUILD_DIR="$ARCH_ROOT/pkgbuild" +SOURCE_ROOT="$ARCH_ROOT/source" +SOURCE_NAME="emerald-${PACKAGE_VERSION}-1-x86_64.tar.gz" + +if [ ! -d "$INSTALL_ROOT/opt/emerald" ]; then + echo "Install root was not found. Run scripts/linux/publish-linux-x64.sh first." + exit 1 +fi + +rm -rf "$ARCH_ROOT" +mkdir -p "$PKGBUILD_DIR" "$SOURCE_ROOT/package" +cp -a "$INSTALL_ROOT/." "$SOURCE_ROOT/package/" +tar -czf "$PKGBUILD_DIR/$SOURCE_NAME" -C "$SOURCE_ROOT" package +SOURCE_SHA256="$(sha256sum "$PKGBUILD_DIR/$SOURCE_NAME" | awk '{print $1}')" + +cat > "$PKGBUILD_DIR/PKGBUILD" < "$INSTALL_ROOT/usr/bin/emerald" <<'EOF' +#!/usr/bin/env sh +exec /opt/emerald/Emerald "$@" +EOF +chmod 755 "$INSTALL_ROOT/usr/bin/emerald" + +install -dm755 "$INSTALL_ROOT/usr/share/applications" +cat > "$INSTALL_ROOT/usr/share/applications/emerald.desktop" <<'EOF' +[Desktop Entry] +Name=Emerald +Comment=Open-source cross-platform Minecraft launcher +Exec=emerald +Icon=emerald +Terminal=false +Type=Application +Categories=Game; +EOF + +install -dm755 "$INSTALL_ROOT/usr/share/icons/hicolor/256x256/apps" +install -m644 "$PWD/Emerald/Assets/icon.png" "$INSTALL_ROOT/usr/share/icons/hicolor/256x256/apps/emerald.png" + +install -dm755 "$INSTALL_ROOT/usr/share/doc/emerald" "$INSTALL_ROOT/usr/share/licenses/emerald" +install -m644 "$PWD/LICENSE.md" "$INSTALL_ROOT/usr/share/doc/emerald/copyright" +install -m644 "$PWD/LICENSE.md" "$INSTALL_ROOT/usr/share/licenses/emerald/LICENSE.md" + +if command -v desktop-file-validate >/dev/null 2>&1; then + desktop-file-validate "$INSTALL_ROOT/usr/share/applications/emerald.desktop" +fi + +install -dm755 "$APPDIR/usr/lib/emerald" "$APPDIR/usr/bin" "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps" +cp -a "$PUBLISH_DIR/." "$APPDIR/usr/lib/emerald/" +install -m644 "$INSTALL_ROOT/usr/share/applications/emerald.desktop" "$APPDIR/usr/share/applications/emerald.desktop" +install -m644 "$INSTALL_ROOT/usr/share/applications/emerald.desktop" "$APPDIR/usr/share/applications/com.riversidevalley.Emerald.desktop" +install -m644 "$INSTALL_ROOT/usr/share/icons/hicolor/256x256/apps/emerald.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/emerald.png" + +cat > "$APPDIR/usr/bin/emerald" <<'EOF' +#!/usr/bin/env sh +APPDIR="${APPDIR:-$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)}" +export LD_LIBRARY_PATH="$APPDIR/usr/lib/emerald${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +exec "$APPDIR/usr/lib/emerald/Emerald" "$@" +EOF +chmod 755 "$APPDIR/usr/bin/emerald"