diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index 7bf8578926d..8a5d132e484 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 }}
@@ -242,9 +245,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:
@@ -258,57 +265,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
@@ -595,8 +592,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
@@ -662,13 +658,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
@@ -692,8 +696,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
@@ -705,8 +711,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
@@ -763,10 +768,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/news/changelog-1.9.md b/news/changelog-1.9.md
index 33c00f631d1..7c00fa71624 100644
--- a/news/changelog-1.9.md
+++ b/news/changelog-1.9.md
@@ -33,3 +33,7 @@ All changes included in 1.9:
### Confluence
- ([#13414](https://github.com/quarto-dev/quarto-cli/issues/13414)): Be more forgiving when Confluence server returns malformed JSON response. (author: @m1no)
+
+## Other fixes and improvements
+
+- ([#13402](https://github.com/quarto-dev/quarto-cli/issues/13402)): `nfpm` () is now used to create the `.deb` package, and new `.rpm` package. Both Linux packages are also now built for `x86_64` (`amd64`) and `aarch64` (`arm64`) architectures.
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..cd90fd65bcb 100644
--- a/package/src/linux/installer.ts
+++ b/package/src/linux/installer.ts
@@ -5,25 +5,112 @@
*
*/
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,
+) {
+ const arch = mapArchitecture(configuration.arch, format);
+ const workingBinPath = join(
+ workingDir,
+ "opt",
+ configuration.productName.toLowerCase(),
+ "bin",
+ );
+ const workingSharePath = join(
+ workingDir,
+ "opt",
+ configuration.productName.toLowerCase(),
+ "share",
+ );
+
+ const contents: any[] = [
+ {
+ src: workingBinPath,
+ dst: "/opt/quarto/bin",
+ type: "tree",
+ },
+ {
+ src: workingSharePath,
+ dst: "/opt/quarto/share",
+ type: "tree",
+ },
+ ];
+
+ // Add copyright file for DEB packages
+ if (format === 'deb') {
+ const copyrightFile = join(
+ workingDir,
+ "usr",
+ "share",
+ "doc",
+ configuration.productName.toLowerCase(),
+ "copyright",
+ );
+ contents.push({
+ src: copyrightFile,
+ dst: `/usr/share/doc/${configuration.productName.toLowerCase()}/copyright`,
+ });
+ }
+
+ 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: contents,
+
+ 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";
+ }
+ return config;
+}
+
+// Build package using nfpm
+async function buildPackageWithNfpm(
configuration: Configuration,
+ format: 'deb' | 'rpm',
) {
- 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 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 +118,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 +141,55 @@ export async function makeInstallerDeb(
overwrite: true,
});
- const val = (name: string, value: string): string => {
- return `${name}: ${value}\n`;
- };
+ // Create copyright file for DEB packages
+ if (format === 'deb') {
+ info("Creating copyright file");
+ const url = "https://github.com/quarto-dev/quarto-cli";
+ const copyrightText = `Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: Quarto
+Source: ${url}
- // 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(","));
+Files: *
+Copyright: Posit, PBC.
+License: MIT`;
+
+ const copyrightDir = join(workingDir, "usr", "share", "doc", configuration.productName.toLowerCase());
+ ensureDirSync(copyrightDir);
+ Deno.writeTextFileSync(join(copyrightDir, "copyright"), copyrightText);
}
- 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);
+ // Create nfpm configuration
+ const nfpmConfig = await createNfpmConfig(configuration, format, workingDir);
+ const configPath = join(configuration.directoryInfo.out, "nfpm.yaml");
- // Write the control file to the DEBIAN directory
- Deno.writeTextFileSync(join(debianDir, "control"), control);
+ info("Creating nfpm configuration file");
+ Deno.writeTextFileSync(configPath, yaml.dump(nfpmConfig));
- // 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 },
- );
-
- 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