#!/bin/bash # # Duenote Linux installer. # # Unlike the macOS installer, this one solves no security-policy problem: # Linux has no Gatekeeper, no quarantine attribute and no signature # enforcement, so an unsigned build installs and runs normally. This exists # purely so users don't have to hand-assemble a versioned URL. # # Three formats are published, and the right one depends on the distro: # # .deb Debian, Ubuntu and derivatives # .rpm Fedora, openSUSE, RHEL and derivatives # .AppImage everything else -- a single executable, installed per-user # with no root and no package manager # # Arch and its derivatives take the AppImage. The .deb cannot serve them: its # dependencies are Debian names pacman knows nothing about, and forcing it in # with dpkg would drop files into /usr that no package owns. # # The package formats register *.2nt as a Duenote notebook from their install # scripts; the AppImage branch below does the same thing by hand in the user's # own XDG directories. # # Usage: # curl -fsSL https://download.duenote.app/install-linux.sh | bash # DUENOTE_VERSION=0.6.2 ... | bash # DUENOTE_FORMAT=appimage ... | bash # force a format set -euo pipefail BASE_URL="${DUENOTE_BASE_URL:-https://download.duenote.app}" info() { printf '\033[1;34m==>\033[0m %s\n' "$1"; } warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$1" >&2; } die() { printf '\033[1;31merror:\033[0m %s\n' "$1" >&2; exit 1; } [ "$(uname -s)" = "Linux" ] || die "This installer is for Linux only." command -v curl >/dev/null 2>&1 || die "curl is required but not installed." # Only linux/x64 is published; the release workflow builds on ubuntu-24.04. case "$(uname -m)" in x86_64|amd64) ;; aarch64|arm64) die "No arm64 build is published. Only x86_64 is available." ;; *) die "Unsupported architecture: $(uname -m)" ;; esac # Which package format a system wants is a property of the distribution, not of # which tools happen to be installed. Probing for a binary gets this wrong in # the one case that matters: Arch ships both dpkg and rpm-tools in its own # repositories, so a machine that has them for unpacking or building would be # handed a Debian package and have it installed behind pacman's back. Read the # family from os-release instead, in a subshell so its VERSION and NAME do not # leak into ours, and treat anything unrecognised as AppImage territory. FORMAT="${DUENOTE_FORMAT:-}" if [ -z "$FORMAT" ]; then DISTRO_TAGS="$(. /etc/os-release 2>/dev/null && printf ' %s %s ' "${ID:-}" "${ID_LIKE:-}")" \ || DISTRO_TAGS="" case "$DISTRO_TAGS" in *" debian "*|*" ubuntu "*) FORMAT="deb" ;; *" fedora "*|*" rhel "*|*" centos "*|*" suse "*|*" opensuse "*|*" mageia "*) FORMAT="rpm" ;; *) FORMAT="appimage" ;; esac fi case "$FORMAT" in deb|rpm|appimage) ;; *) die "Unknown DUENOTE_FORMAT: $FORMAT (expected deb, rpm or appimage)" ;; esac # Same version marker the macOS installer uses; written by the release # workflow once every platform has built. VERSION="${DUENOTE_VERSION:-}" if [ -z "$VERSION" ]; then info "Resolving latest release..." VERSION="$(curl -fsSL "${BASE_URL%/}/latest.txt" 2>/dev/null | tr -d '[:space:]')" [ -n "$VERSION" ] || die "Could not resolve the latest version. Set DUENOTE_VERSION=x.y.z and retry." fi # The version lands in a URL path, so refuse anything that is not a plain # version string rather than interpolating whatever the marker file held. case "$VERSION" in *[!0-9.]*|''|.*|*.) die "Unexpected version string: $VERSION" ;; esac # Every maker names its output differently, and the publisher keys objects by # platform and arch: ///. case "$FORMAT" in # electron-installer-debian lowercases the package name. deb) FILE="duenote_${VERSION}_amd64.deb" ;; # electron-installer-redhat appends the release number and uses rpm's # spelling of the architecture. rpm) FILE="duenote-${VERSION}-1.x86_64.rpm" ;; # The AppImage maker keeps productName and the Node spelling of the arch. appimage) FILE="Duenote-${VERSION}-x64.AppImage" ;; esac URL="${BASE_URL%/}/Duenote/linux/x64/${FILE}" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT info "Downloading Duenote ${VERSION} (${FORMAT})..." curl -fL --progress-bar "$URL" -o "$TMP/$FILE" || die "Download failed: $URL" # The package formats need root; the AppImage is installed into the user's own # directories and must not be. if [ "$FORMAT" != "appimage" ]; then SUDO="" if [ "$(id -u)" -ne 0 ]; then command -v sudo >/dev/null 2>&1 || die "Run as root, or install sudo." SUDO="sudo" fi fi case "$FORMAT" in deb) # Reached by a Debian-family system, or by an explicit DUENOTE_FORMAT=deb. # In the second case the tooling may simply not be there, and saying so # beats the "not a valid .deb" that a missing dpkg-deb would produce. command -v dpkg-deb >/dev/null 2>&1 \ || die "dpkg is required to install the .deb. Use DUENOTE_FORMAT=appimage instead." # Catch a truncated or HTML-error-page download before handing it to dpkg. dpkg-deb --info "$TMP/$FILE" >/dev/null 2>&1 || die "Downloaded file is not a valid .deb." info "Installing (requires root)..." if command -v apt-get >/dev/null 2>&1; then # apt-get resolves the bundled dependencies; dpkg alone would leave them # unconfigured on a minimal system. $SUDO apt-get install -y "$TMP/$FILE" else $SUDO dpkg -i "$TMP/$FILE" || die "dpkg reported unmet dependencies; install them and retry." fi ;; rpm) command -v rpm >/dev/null 2>&1 \ || die "rpm is required to install the .rpm. Use DUENOTE_FORMAT=appimage instead." rpm -qp "$TMP/$FILE" >/dev/null 2>&1 || die "Downloaded file is not a valid .rpm." info "Installing (requires root)..." # dnf and zypper pull in the Electron dependencies; plain rpm would stop at # the first unmet one. `install` upgrades an existing Duenote in both. if command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y "$TMP/$FILE" elif command -v zypper >/dev/null 2>&1; then $SUDO zypper --non-interactive install --allow-unsigned-rpm "$TMP/$FILE" elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y "$TMP/$FILE" else $SUDO rpm -Uvh "$TMP/$FILE" || die "rpm reported unmet dependencies; install them and retry." fi ;; appimage) # No package manager and no root: the image goes in the user's data # directory, with a symlink on PATH and a desktop entry so it shows up in # the application menu like the packaged builds do. DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}" APP_DIR="$DATA_DIR/duenote" BIN_DIR="$HOME/.local/bin" TARGET="$APP_DIR/Duenote.AppImage" mkdir -p "$APP_DIR" "$BIN_DIR" "$DATA_DIR/applications" "$DATA_DIR/mime/packages" info "Installing to $TARGET..." install -m 755 "$TMP/$FILE" "$TARGET" ln -sf "$TARGET" "$BIN_DIR/duenote" # Best effort: the icon lives inside the image, and pulling it out needs # the runtime to cooperate. A menu entry without an icon still works, so a # failure here is not worth aborting an otherwise complete install. ICON_LINE="Icon=duenote" if (cd "$TMP" && "$TARGET" --appimage-extract 'usr/share/icons/hicolor/*/apps/*' >/dev/null 2>&1); then ICON_SRC="$(find "$TMP/squashfs-root" -name 'duenote.*' -print -quit 2>/dev/null || true)" if [ -n "$ICON_SRC" ]; then mkdir -p "$DATA_DIR/icons/hicolor/512x512/apps" cp "$ICON_SRC" "$DATA_DIR/icons/hicolor/512x512/apps/duenote.${ICON_SRC##*.}" else ICON_LINE="" fi else ICON_LINE="" fi cat > "$DATA_DIR/applications/duenote.desktop" < "$DATA_DIR/mime/packages/duenote.xml" <<'EOF' Duenote notebook EOF command -v update-mime-database >/dev/null 2>&1 && update-mime-database "$DATA_DIR/mime" || true command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$DATA_DIR/applications" || true case ":$PATH:" in *":$BIN_DIR:"*) ;; *) warn "$BIN_DIR is not on your PATH; add it to run 'duenote' from a shell." ;; esac # The type-2 runtime mounts itself with FUSE. Distros that ship only # fuse3 leave AppImages failing with a bare "dlopen(): error loading # libfuse.so.2", which is worth naming before the user hits it. if ! ldconfig -p 2>/dev/null | grep -q 'libfuse\.so\.2'; then warn "libfuse2 was not found. If Duenote fails to start, install it (for example 'libfuse2' or 'fuse2'), or run: $TARGET --appimage-extract-and-run" fi ;; esac info "Done. Launch Duenote from your application menu, or run: duenote"