diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index db427db6a95..aa1f7896338 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -15,6 +15,9 @@ on: type: boolean default: true +env: + NFPM_VERSION: "2.43.1" + concurrency: # make publishing release concurrent (but others trigger not) group: building-releases-${{ inputs.publish-release && 'prerelease' || github.run_id }} @@ -240,9 +243,13 @@ jobs: name: RHEL Zip path: ./package/quarto-${{needs.configure.outputs.version}}-linux-rhel7-amd64.tar.gz - make-installer-arm64-deb: + make-installer-linux: runs-on: ubuntu-latest needs: [configure] + strategy: + matrix: + arch: [x86_64, aarch64] + format: [deb, rpm] steps: - uses: actions/checkout@v4 with: @@ -256,57 +263,47 @@ jobs: run: | ./configure.sh - - name: Prepare Distribution - run: | - pushd package/src/ - ./quarto-bld prepare-dist --set-version ${{needs.configure.outputs.version}} --arch aarch64 --log-level info - popd - - - name: Make Installer - run: | - pushd package/src/ - ./quarto-bld make-installer-deb --set-version ${{needs.configure.outputs.version}} --arch aarch64 --log-level info - popd - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: Deb Arm64 Installer - path: ./package/out/quarto-${{needs.configure.outputs.version}}-linux-arm64.deb - - make-installer-deb: - runs-on: ubuntu-latest - needs: [configure] - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.configure.outputs.version_commit }} - - - name: Prevent Re-run - if: ${{ inputs.publish-release }} - uses: ./.github/workflows/actions/prevent-rerun - - - name: Configure + - name: Install nfpm run: | - ./configure.sh + wget -q https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz + tar -xzf nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz + sudo mv nfpm /usr/local/bin/ + nfpm --version - name: Prepare Distribution run: | pushd package/src/ - ./quarto-bld prepare-dist --set-version ${{needs.configure.outputs.version}} --log-level info + ./quarto-bld prepare-dist --set-version ${{needs.configure.outputs.version}} ${{ matrix.arch == 'aarch64' && '--arch aarch64' || '' }} --log-level info popd - name: Make Installer run: | pushd package/src/ - ./quarto-bld make-installer-deb --set-version ${{needs.configure.outputs.version}} --log-level info + ./quarto-bld make-installer-${{ matrix.format }} --set-version ${{needs.configure.outputs.version}} ${{ matrix.arch == 'aarch64' && '--arch aarch64' || '' }} --log-level info popd + - name: Set package architecture name + id: pkg_arch + run: | + if [ "${{ matrix.format }}" == "deb" ]; then + if [ "${{ matrix.arch }}" == "x86_64" ]; then + echo "arch_name=amd64" >> $GITHUB_OUTPUT + else + echo "arch_name=arm64" >> $GITHUB_OUTPUT + fi + else + if [ "${{ matrix.arch }}" == "x86_64" ]; then + echo "arch_name=x86_64" >> $GITHUB_OUTPUT + else + echo "arch_name=aarch64" >> $GITHUB_OUTPUT + fi + fi + - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: Deb Installer - path: ./package/out/quarto-${{needs.configure.outputs.version}}-linux-amd64.deb + name: Linux-${{ matrix.format }}-${{ matrix.arch }}-Installer + path: ./package/out/quarto-${{needs.configure.outputs.version}}-linux-${{ steps.pkg_arch.outputs.arch_name }}.${{ matrix.format }} test-tarball-linux: runs-on: ubuntu-latest @@ -593,8 +590,7 @@ jobs: runs-on: ubuntu-latest needs: [ configure, - make-installer-deb, - make-installer-arm64-deb, + make-installer-linux, make-installer-win, make-installer-mac, # optional in release to not be blocked by RHEL build depending on conda-forge deno dependency @@ -660,13 +656,21 @@ jobs: sha256sum quarto-${{needs.configure.outputs.version}}-linux-arm64.tar.gz >> ../quarto-${{needs.configure.outputs.version}}-checksums.txt popd - pushd Deb\ Installer + pushd Linux-deb-x86_64-Installer sha256sum quarto-${{needs.configure.outputs.version}}-linux-amd64.deb >> ../quarto-${{needs.configure.outputs.version}}-checksums.txt popd - pushd Deb\ Arm64\ Installer + pushd Linux-deb-aarch64-Installer sha256sum quarto-${{needs.configure.outputs.version}}-linux-arm64.deb >> ../quarto-${{needs.configure.outputs.version}}-checksums.txt - popd + popd + + pushd Linux-rpm-x86_64-Installer + sha256sum quarto-${{needs.configure.outputs.version}}-linux-x86_64.rpm >> ../quarto-${{needs.configure.outputs.version}}-checksums.txt + popd + + pushd Linux-rpm-aarch64-Installer + sha256sum quarto-${{needs.configure.outputs.version}}-linux-aarch64.rpm >> ../quarto-${{needs.configure.outputs.version}}-checksums.txt + popd pushd Source sha256sum quarto-${{needs.configure.outputs.version}}.tar.gz >> ../quarto-${{needs.configure.outputs.version}}-checksums.txt @@ -690,8 +694,10 @@ jobs: ./Deb Zip/quarto-${{needs.configure.outputs.version}}-linux-amd64.tar.gz ./Deb Arm64 Zip/quarto-${{needs.configure.outputs.version}}-linux-arm64.tar.gz ./RHEL Zip/quarto-${{needs.configure.outputs.version}}-linux-rhel7-amd64.tar.gz - ./Deb Installer/quarto-${{needs.configure.outputs.version}}-linux-amd64.deb - ./Deb Arm64 Installer/quarto-${{needs.configure.outputs.version}}-linux-arm64.deb + ./Linux-deb-x86_64-Installer/quarto-${{needs.configure.outputs.version}}-linux-amd64.deb + ./Linux-deb-aarch64-Installer/quarto-${{needs.configure.outputs.version}}-linux-arm64.deb + ./Linux-rpm-x86_64-Installer/quarto-${{needs.configure.outputs.version}}-linux-x86_64.rpm + ./Linux-rpm-aarch64-Installer/quarto-${{needs.configure.outputs.version}}-linux-aarch64.rpm ./Windows Installer/quarto-${{needs.configure.outputs.version}}-win.msi ./Windows Zip/quarto-${{needs.configure.outputs.version}}-win.zip ./Mac Installer/quarto-${{needs.configure.outputs.version}}-macos.pkg @@ -703,8 +709,7 @@ jobs: if: ${{ (failure() || cancelled()) && inputs.publish-release }} needs: [ configure, - make-installer-deb, - make-installer-arm64-deb, + make-installer-linux, make-installer-win, make-installer-mac, # optional in release to not be blocked by RHEL build depending on conda-forge deno dependency @@ -761,10 +766,62 @@ jobs: - uses: ./.github/actions/docker with: - source: ./Deb Installer/quarto-${{needs.configure.outputs.version}}-linux-amd64.deb + source: ./Linux-deb-x86_64-Installer/quarto-${{needs.configure.outputs.version}}-linux-amd64.deb version: ${{needs.configure.outputs.version}} token: ${{ secrets.GITHUB_TOKEN }} username: ${{ github.actor }} org: ${{ github.repository_owner }} name: quarto daily: ${{ inputs.pre-release }} + + cloudsmith-push: + if: ${{ inputs.publish-release }} + runs-on: ubuntu-latest + needs: [configure, publish-release] + strategy: + matrix: + arch: [x86_64, aarch64] + format: [deb, rpm] + repo: [open, pro] + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + .github + + - name: Prevent Re-run + if: ${{ inputs.publish-release }} + uses: ./.github/workflows/actions/prevent-rerun + + - name: Download Artifacts + uses: actions/download-artifact@v4 + + - name: Set package file name + id: pkg_file + run: | + if [ "${{ matrix.format }}" == "deb" ]; then + if [ "${{ matrix.arch }}" == "x86_64" ]; then + echo "arch_name=amd64" >> $GITHUB_OUTPUT + else + echo "arch_name=arm64" >> $GITHUB_OUTPUT + fi + else + if [ "${{ matrix.arch }}" == "x86_64" ]; then + echo "arch_name=x86_64" >> $GITHUB_OUTPUT + else + echo "arch_name=aarch64" >> $GITHUB_OUTPUT + fi + fi + + - name: Push ${{ matrix.format }} ${{ matrix.arch }} to Cloudsmith ${{ matrix.repo }} + uses: cloudsmith-io/action@master + with: + api-key: ${{ secrets.CLOUDSMITH_API_KEY }} + command: "push" + format: "${{ matrix.format }}" + owner: "posit" + repo: "${{ matrix.repo }}" + distro: "any-distro" + release: "any-version" + republish: "true" + file: "./Linux-${{ matrix.format }}-${{ matrix.arch }}-Installer/quarto-${{needs.configure.outputs.version}}-linux-${{ steps.pkg_file.outputs.arch_name }}.${{ matrix.format }}" diff --git a/package/scripts/linux/rpm/postinst b/package/scripts/linux/rpm/postinst new file mode 100644 index 00000000000..d5dce53bf47 --- /dev/null +++ b/package/scripts/linux/rpm/postinst @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -e + +# detect whether running as root (per machine installation) +# if per machine (run without sudo): + +if [[ $EUID -eq 0 ]]; then + if [ -d "/usr/local/bin" ] + then + ln -fs /opt/quarto/bin/quarto /usr/local/bin/quarto + else + echo "Quarto symlink not created, please be sure that you add Quarto to your path." + fi + + if [ -d "/usr/local/man/man1" ] + then + ln -fs /opt/quarto/share/man/quarto.man /usr/local/man/man1/quarto.1 + elif [ -d "/usr/local/man" ] + then + ln -fs /opt/quarto/share/man/quarto.man /usr/local/man/quarto.1 + fi + +else + if [ -d "~/bin/quarto" ] + then + ln -fs /opt/quarto/bin/quarto ~/bin/quarto + else + echo "Quarto symlink not created, please be sure that you add Quarto to your path." + fi + + if [ -d "~/man/man1" ] + then + ln -fs /opt/quarto/share/man/quarto.man ~/man/man1/quarto.1 + elif [ -d "~/man" ] + then + ln -fs /opt/quarto/share/man/quarto.man ~/man/quarto.1 + fi + +fi + +# Figure architecture +NIXARCH=$(uname -m) +if [[ $NIXARCH == "aarch64" ]]; then + ARCH_DIR=aarch64 +else + ARCH_DIR=x86_64 +fi + +ln -fs /opt/quarto/bin/tools/${ARCH_DIR}/pandoc /opt/quarto/bin/tools/pandoc + +exit 0 \ No newline at end of file diff --git a/package/scripts/linux/rpm/postrm b/package/scripts/linux/rpm/postrm new file mode 100644 index 00000000000..cac6088cdd1 --- /dev/null +++ b/package/scripts/linux/rpm/postrm @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e + +if [[ "$EUID" -eq 0 ]] +then +rm -f /usr/local/bin/quarto +else +rm -f ~/bin/quarto +fi + +# Remove pandoc symlink created by postinst +# (before 1.4 this was a regular file that shouldn't be removed here) +pandoc=/opt/quarto/bin/tools/pandoc +if [ -h "$pandoc" ] +then + rm -f "$pandoc" +fi + +exit 0 \ No newline at end of file diff --git a/package/src/bld.ts b/package/src/bld.ts index 7bdbc86861f..ff69ab48c73 100644 --- a/package/src/bld.ts +++ b/package/src/bld.ts @@ -10,7 +10,7 @@ import { mainRunner } from "../../src/core/main.ts"; import { prepareDist } from "./common/prepare-dist.ts"; import { updateHtmlDependencies } from "./common/update-html-dependencies.ts"; -import { makeInstallerDeb } from "./linux/installer.ts"; +import { makeInstallerDeb, makeInstallerRpm } from "./linux/installer.ts"; import { makeInstallerMac } from "./macos/installer.ts"; import { compileQuartoLatexmkCommand, @@ -96,6 +96,10 @@ function getCommands() { packageCommand(makeInstallerDeb, "make-installer-deb") .description("Builds Linux deb installer"), ); + commands.push( + packageCommand(makeInstallerRpm, "make-installer-rpm") + .description("Builds Linux rpm installer"), + ); commands.push( packageCommand(makeInstallerWindows, "make-installer-win") .description("Builds Windows installer"), diff --git a/package/src/linux/installer.ts b/package/src/linux/installer.ts index fa0e6e0d90c..89a5a0ed5cd 100644 --- a/package/src/linux/installer.ts +++ b/package/src/linux/installer.ts @@ -5,25 +5,111 @@ * */ import { join } from "../../../src/deno_ral/path.ts"; -import { copySync, emptyDirSync, ensureDirSync, walk } from "../../../src/deno_ral/fs.ts"; +import { copySync, emptyDirSync, ensureDirSync, existsSync, walk } from "../../../src/deno_ral/fs.ts"; import { info } from "../../../src/deno_ral/log.ts"; +import * as yaml from "../../../src/core/lib/external/js-yaml.js"; import { Configuration } from "../common/config.ts"; import { runCmd } from "../util/cmd.ts"; -export async function makeInstallerDeb( +// Map architecture names between Quarto and package formats +function mapArchitecture(arch: string, format: 'deb' | 'rpm'): string { + if (format === 'deb') { + return arch === 'x86_64' ? 'amd64' : 'arm64'; + } else { // rpm + return arch === 'x86_64' ? 'x86_64' : 'aarch64'; + } +} + +// Create nfpm configuration for DEB or RPM packages +async function createNfpmConfig( configuration: Configuration, + format: 'deb' | 'rpm', + workingDir: string, ) { - info("Building deb package..."); - - // detect packaging machine architecture - // See complete list dpkg-architecture -L. - // arm64 - // amd64 - const architecture = configuration.arch === "x86_64" ? "amd64" : "arm64"; - const packageName = - `quarto-${configuration.version}-linux-${architecture}.deb`; - info("Building package " + packageName); + const arch = mapArchitecture(configuration.arch, format); + const workingBinPath = join( + workingDir, + "opt", + configuration.productName.toLowerCase(), + "bin", + ); + const workingSharePath = join( + workingDir, + "opt", + configuration.productName.toLowerCase(), + "share", + ); + + // Calculate installed size + const fileSizes = []; + for await (const entry of walk(configuration.directoryInfo.pkgWorking.root)) { + if (entry.isFile) { + fileSizes.push((await Deno.stat(entry.path)).size); + } + } + const size = fileSizes.reduce((accum, target) => accum + target, 0); + + const config: any = { + name: configuration.productName.toLowerCase(), + version: configuration.version, + arch: arch, + maintainer: "Posit, PBC ", + description: "Quarto is an academic, scientific, and technical publishing system built on Pandoc.", + homepage: "https://github.com/quarto-dev/quarto-cli", + license: "MIT", + + contents: [ + { + src: workingBinPath, + dst: "/opt/quarto/bin", + type: "tree", + }, + { + src: workingSharePath, + dst: "/opt/quarto/share", + type: "tree", + }, + ], + + scripts: { + postinstall: join(configuration.directoryInfo.pkg, "scripts", "linux", format, "postinst"), + postremove: join(configuration.directoryInfo.pkg, "scripts", "linux", format, "postrm"), + }, + + overrides: {}, + }; + + // Format-specific configuration + if (format === 'deb') { + config.overrides.deb = { + recommends: ["unzip"], + }; + // Add Debian-specific metadata + config.section = "user/text"; + config.priority = "optional"; + config.installed_size = Math.round(size / 1024); + } else if (format === 'rpm') { + config.overrides.rpm = { + compression: "gzip", + summary: "Academic, scientific, and technical publishing system", + group: "Applications/Publishing", + }; + } + + return config; +} + +// Build package using nfpm +async function buildPackageWithNfpm( + configuration: Configuration, + format: 'deb' | 'rpm', +) { + const packageExt = format === 'deb' ? 'deb' : 'rpm'; + const arch = mapArchitecture(configuration.arch, format); + const packageName = `quarto-${configuration.version}-linux-${arch}.${packageExt}`; + + info(`Building ${format.toUpperCase()} package: ${packageName}`); // Prepare working directory const workingDir = join(configuration.directoryInfo.out, "working"); @@ -31,7 +117,7 @@ export async function makeInstallerDeb( ensureDirSync(workingDir); emptyDirSync(workingDir); - // Copy bin into the proper path in working dir + // Copy bin and share directories const workingBinPath = join( workingDir, "opt", @@ -54,85 +140,38 @@ export async function makeInstallerDeb( overwrite: true, }); - const val = (name: string, value: string): string => { - return `${name}: ${value}\n`; - }; + // Create nfpm configuration + const nfpmConfig = await createNfpmConfig(configuration, format, workingDir); + const configPath = join(configuration.directoryInfo.out, "nfpm.yaml"); - // Calculate the install size - const fileSizes = []; - for await (const entry of walk(configuration.directoryInfo.pkgWorking.root)) { - if (entry.isFile) { - fileSizes.push((await Deno.stat(entry.path)).size); - } - } - const size = fileSizes.reduce((accum, target) => { - return accum + target; - }); - const url = "https://github.com/quarto-dev/quarto-cli"; - const recommends = ["unzip"]; - - // Make the control file - info("Creating control file"); - let control = ""; - control = control + val("Package", configuration.productName); - if (recommends.length) { - control = control + val("Recommends", recommends.join(",")); - } - control = control + val("Version", configuration.version); - control = control + val("Architecture", architecture); - control = control + val("Installed-Size", `${Math.round(size / 1024)}`); - control = control + val("Section", "user/text"); - control = control + val("Priority", "optional"); - control = control + val("Maintainer", "Posit, PBC "); - control = control + val("Homepage", url); - control = control + - val( - "Description", - "Quarto is an academic, scientific, and technical publishing system built on Pandoc.", - ); - info(control); - - // Place - const debianDir = join(workingDir, "DEBIAN"); - ensureDirSync(debianDir); - - // Write the control file to the DEBIAN directory - Deno.writeTextFileSync(join(debianDir, "control"), control); - - // Generate and write a copyright file - info("Creating copyright file"); - const copyrightLines = []; - copyrightLines.push( - "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/", - ); - copyrightLines.push("Upstream-Name: Quarto"); - copyrightLines.push(`Source: ${url}`); - copyrightLines.push(""); - copyrightLines.push("Files: *"); - copyrightLines.push("Copyright: Posit, PBC."); - copyrightLines.push("License: MIT"); - const copyrightText = copyrightLines.join("\n"); - Deno.writeTextFileSync(join(debianDir, "copyright"), copyrightText); - - // copy the install scripts - info("Copying install scripts..."); - copySync( - join(configuration.directoryInfo.pkg, "scripts", "linux", "deb"), - debianDir, - { overwrite: true }, - ); + info("Creating nfpm configuration file"); + Deno.writeTextFileSync(configPath, yaml.dump(nfpmConfig)); - await runCmd("dpkg-deb", [ - "-Z", - "gzip", - "-z", - "9", - "--root-owner-group", - "--build", - workingDir, - join(configuration.directoryInfo.out, packageName), + // Build package using nfpm (assumes nfpm is installed in PATH) + const outputPath = join(configuration.directoryInfo.out, packageName); + await runCmd("nfpm", [ + "package", + "--config", configPath, + "--target", outputPath, + "--packager", format, ]); - // Remove the working directory + info(`Package created: ${outputPath}`); + + // Clean up + Deno.removeSync(configPath); + // Optionally remove working directory // Deno.removeSync(workingDir, { recursive: true }); } + +export async function makeInstallerDeb( + configuration: Configuration, +) { + await buildPackageWithNfpm(configuration, 'deb'); +} + +export async function makeInstallerRpm( + configuration: Configuration, +) { + await buildPackageWithNfpm(configuration, 'rpm'); +} \ No newline at end of file