fix: macOS and Windows test/lint failures; repo.dixon.cx packaging for nexusterminal
CI hadn't run on macOS or Windows for a while (billing); once it did: - Windows: the RDP shared-drive code and its symlink test are Unix-only but were compiled there (and the unused pieces failed clippy). The drive type, its impls, imports and test are now cfg(unix); refusing a drive request is a free function used everywhere. Other Windows-only warnings fixed (serial test import, unused mut, harden_process). - macOS: the local shell's directory was sampled on the first output after Enter, which is the shell echoing the command before a cd runs, so restored tabs opened one directory behind. It's now sampled shortly after Enter and again once the command has had time to finish. - packaging/repo.sh publishes the 'nexusterminal' .deb/.rpm/Arch packages to repo.dixon.cx from the release's own Linux binaries, verified against SHA256SUMS. - The desktop entry mentions RDP.
This commit is contained in:
@@ -175,10 +175,8 @@ fn init_logging(data_dir: &std::path::Path) {
|
|||||||
/// files, and on Linux the process is non-dumpable (also blocks ptrace attach by other processes
|
/// files, and on Linux the process is non-dumpable (also blocks ptrace attach by other processes
|
||||||
/// of the same user). Debug builds skip this so they can still be debugged.
|
/// of the same user). Debug builds skip this so they can still be debugged.
|
||||||
fn harden_process() {
|
fn harden_process() {
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
// SAFETY: plain syscalls on this process with valid arguments; failure is harmless.
|
// SAFETY: plain syscalls on this process with valid arguments; failure is harmless.
|
||||||
unsafe {
|
unsafe {
|
||||||
let none = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
|
let none = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
|
||||||
@@ -187,3 +185,4 @@ fn harden_process() {
|
|||||||
libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0);
|
libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ impl LocalShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// macOS has no cheap cwd lookup (it's an `lsof` run), so the directory is sampled after Enter:
|
||||||
|
/// the first output after Enter is usually the shell echoing the command, before a `cd` has
|
||||||
|
/// run, so sample shortly afterwards and again once the command has had time to finish.
|
||||||
fn capture_cwd_in_background(self: &Arc<Self>) {
|
fn capture_cwd_in_background(self: &Arc<Self>) {
|
||||||
let Some(pid) = self.state.lock().0.as_ref().and_then(|r| r.pid) else { return };
|
let Some(pid) = self.state.lock().0.as_ref().and_then(|r| r.pid) else { return };
|
||||||
if self.capturing.swap(true, Ordering::SeqCst) {
|
if self.capturing.swap(true, Ordering::SeqCst) {
|
||||||
@@ -178,10 +181,13 @@ impl LocalShell {
|
|||||||
}
|
}
|
||||||
let me = self.clone();
|
let me = self.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
for delay in [150, 850] {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(delay));
|
||||||
// A failed lookup (the shell already exited) keeps the last known directory.
|
// A failed lookup (the shell already exited) keeps the last known directory.
|
||||||
if let Some(dir) = process_cwd(pid).filter(|d| d.is_dir()) {
|
if let Some(dir) = process_cwd(pid).filter(|d| d.is_dir()) {
|
||||||
me.state.lock().1 = dir;
|
me.state.lock().1 = dir;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
me.capturing.store(false, Ordering::SeqCst);
|
me.capturing.store(false, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -223,6 +229,11 @@ mod tests {
|
|||||||
std::thread::sleep(Duration::from_millis(20));
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
}
|
}
|
||||||
sh.resize(100, 30).unwrap();
|
sh.resize(100, 30).unwrap();
|
||||||
|
// macOS samples the directory shortly after a command runs (Linux reads it live).
|
||||||
|
while sh.working_directory() != std::path::Path::new("/") {
|
||||||
|
assert!(t.elapsed() < Duration::from_secs(5), "cwd still {:?}", sh.working_directory());
|
||||||
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
sh.write(b"exit\n").unwrap();
|
sh.write(b"exit\n").unwrap();
|
||||||
while *closed.lock() == 0 {
|
while *closed.lock() == 0 {
|
||||||
assert!(t.elapsed() < Duration::from_secs(5));
|
assert!(t.elapsed() < Duration::from_secs(5));
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
if self.cfg.shared_folder.is_some() || self.cfg.printer.is_some() {
|
if self.cfg.shared_folder.is_some() || self.cfg.printer.is_some() {
|
||||||
let mut devices = super::rdp_devices::Devices::default();
|
let mut devices = super::rdp_devices::Devices::default();
|
||||||
|
#[cfg_attr(not(unix), allow(unused_mut))]
|
||||||
let mut drives = vec![];
|
let mut drives = vec![];
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some((folder, name)) = &self.cfg.shared_folder {
|
if let Some((folder, name)) = &self.cfg.shared_folder {
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ use ironrdp::core::impl_as_any;
|
|||||||
use ironrdp::pdu::PduResult;
|
use ironrdp::pdu::PduResult;
|
||||||
use ironrdp::rdpdr::RdpdrBackend;
|
use ironrdp::rdpdr::RdpdrBackend;
|
||||||
use ironrdp::rdpdr::pdu::RdpdrPdu;
|
use ironrdp::rdpdr::pdu::RdpdrPdu;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use ironrdp::rdpdr::pdu::efs::FileInformationClass;
|
||||||
use ironrdp::rdpdr::pdu::efs::{
|
use ironrdp::rdpdr::pdu::efs::{
|
||||||
ClientDriveQueryDirectoryResponse, ClientDriveSetInformationResponse, DeviceCloseResponse, DeviceControlRequest, DeviceCreateResponse, DeviceIoResponse,
|
ClientDriveQueryDirectoryResponse, ClientDriveSetInformationResponse, DeviceCloseResponse, DeviceControlRequest, DeviceCreateResponse, DeviceIoResponse,
|
||||||
DeviceWriteResponse, FileInformationClass, Information, NtStatus, PrinterIoRequest, ServerDeviceAnnounceResponse, ServerDriveIoRequest,
|
DeviceWriteResponse, Information, NtStatus, PrinterIoRequest, ServerDeviceAnnounceResponse, ServerDriveIoRequest,
|
||||||
};
|
};
|
||||||
use ironrdp::rdpdr::pdu::esc::{ScardCall, ScardIoCtlCode};
|
use ironrdp::rdpdr::pdu::esc::{ScardCall, ScardIoCtlCode};
|
||||||
use ironrdp::svc::SvcMessage;
|
use ironrdp::svc::SvcMessage;
|
||||||
@@ -24,7 +26,9 @@ use ironrdp::svc::SvcMessage;
|
|||||||
use ironrdp_rdpdr_native::backend::NixRdpdrBackend;
|
use ironrdp_rdpdr_native::backend::NixRdpdrBackend;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
#[cfg(unix)]
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// A print job may not grow past this (a hostile server could otherwise fill the disk).
|
/// A print job may not grow past this (a hostile server could otherwise fill the disk).
|
||||||
const MAX_PRINT_JOB: u64 = 256 << 20;
|
const MAX_PRINT_JOB: u64 = 256 << 20;
|
||||||
@@ -32,6 +36,7 @@ const MAX_PRINT_JOB: u64 = 256 << 20;
|
|||||||
const MAX_OPEN_PRINT_JOBS: usize = 4;
|
const MAX_OPEN_PRINT_JOBS: usize = 4;
|
||||||
/// Largest drive read served in one request. The server picks the length (a u32), and the
|
/// Largest drive read served in one request. The server picks the length (a u32), and the
|
||||||
/// native backend allocates it before reading; Windows clients never ask for more than ~1 MiB.
|
/// native backend allocates it before reading; Windows clients never ask for more than ~1 MiB.
|
||||||
|
#[cfg(unix)]
|
||||||
const MAX_DRIVE_READ: u32 = 8 << 20;
|
const MAX_DRIVE_READ: u32 = 8 << 20;
|
||||||
|
|
||||||
/// Called with the file a finished print job was saved to.
|
/// Called with the file a finished print job was saved to.
|
||||||
@@ -151,7 +156,7 @@ impl RdpdrBackend for Devices {
|
|||||||
if let Some(d) = &mut self.drive {
|
if let Some(d) = &mut self.drive {
|
||||||
return d.handle(req);
|
return d.handle(req);
|
||||||
}
|
}
|
||||||
ConfinedDrive::refuse(req)
|
refuse_drive_io(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_printer_io_request(&mut self, req: PrinterIoRequest) -> PduResult<Vec<SvcMessage>> {
|
fn handle_printer_io_request(&mut self, req: PrinterIoRequest) -> PduResult<Vec<SvcMessage>> {
|
||||||
@@ -164,14 +169,35 @@ impl RdpdrBackend for Devices {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Answers a drive request with a refusal (no shared folder, or a path outside it).
|
||||||
|
fn refuse_drive_io(req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
|
||||||
|
tracing::warn!("RDP server asked for a path outside the shared folder; refused");
|
||||||
|
let pdu = match req {
|
||||||
|
ServerDriveIoRequest::ServerCreateDriveRequest(r) => RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse {
|
||||||
|
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::ACCESS_DENIED),
|
||||||
|
file_id: 0,
|
||||||
|
information: Information::empty(),
|
||||||
|
}),
|
||||||
|
ServerDriveIoRequest::ServerDriveQueryDirectoryRequest(r) => RdpdrPdu::ClientDriveQueryDirectoryResponse(ClientDriveQueryDirectoryResponse {
|
||||||
|
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::NO_MORE_FILES),
|
||||||
|
buffer: None,
|
||||||
|
}),
|
||||||
|
ServerDriveIoRequest::ServerDriveSetInformationRequest(r) => RdpdrPdu::ClientDriveSetInformationResponse(
|
||||||
|
ClientDriveSetInformationResponse::new(&r, NtStatus::ACCESS_DENIED).map_err(|e| ironrdp::pdu::encode_err!(e))?,
|
||||||
|
),
|
||||||
|
_ => return Ok(vec![]),
|
||||||
|
};
|
||||||
|
Ok(vec![SvcMessage::from(pdu)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ConfinedDrive {
|
struct ConfinedDrive {
|
||||||
#[cfg(unix)]
|
|
||||||
inner: NixRdpdrBackend,
|
inner: NixRdpdrBackend,
|
||||||
#[cfg_attr(not(unix), allow(dead_code))]
|
|
||||||
base: PathBuf,
|
base: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
impl ConfinedDrive {
|
impl ConfinedDrive {
|
||||||
pub fn new(folder: &Path) -> std::io::Result<ConfinedDrive> {
|
pub fn new(folder: &Path) -> std::io::Result<ConfinedDrive> {
|
||||||
let base = folder.canonicalize()?;
|
let base = folder.canonicalize()?;
|
||||||
@@ -211,28 +237,9 @@ impl ConfinedDrive {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refuse(req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
|
|
||||||
tracing::warn!("RDP server asked for a path outside the shared folder; refused");
|
|
||||||
let pdu = match req {
|
|
||||||
ServerDriveIoRequest::ServerCreateDriveRequest(r) => RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse {
|
|
||||||
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::ACCESS_DENIED),
|
|
||||||
file_id: 0,
|
|
||||||
information: Information::empty(),
|
|
||||||
}),
|
|
||||||
ServerDriveIoRequest::ServerDriveQueryDirectoryRequest(r) => RdpdrPdu::ClientDriveQueryDirectoryResponse(ClientDriveQueryDirectoryResponse {
|
|
||||||
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::NO_MORE_FILES),
|
|
||||||
buffer: None,
|
|
||||||
}),
|
|
||||||
ServerDriveIoRequest::ServerDriveSetInformationRequest(r) => RdpdrPdu::ClientDriveSetInformationResponse(
|
|
||||||
ClientDriveSetInformationResponse::new(&r, NtStatus::ACCESS_DENIED).map_err(|e| ironrdp::pdu::encode_err!(e))?,
|
|
||||||
),
|
|
||||||
_ => return Ok(vec![]),
|
|
||||||
};
|
|
||||||
Ok(vec![SvcMessage::from(pdu)])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
impl ConfinedDrive {
|
impl ConfinedDrive {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn handle(&mut self, mut req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
|
fn handle(&mut self, mut req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
|
||||||
@@ -250,7 +257,7 @@ impl ConfinedDrive {
|
|||||||
// Everything else names a file by an id that a checked create handed out.
|
// Everything else names a file by an id that a checked create handed out.
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
if ok { self.inner.handle_drive_io_request(req) } else { Self::refuse(req) }
|
if ok { self.inner.handle_drive_io_request(req) } else { refuse_drive_io(req) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +321,7 @@ mod tests {
|
|||||||
std::fs::remove_dir_all(dir).unwrap();
|
std::fs::remove_dir_all(dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn server_paths_stay_inside_the_share() {
|
fn server_paths_stay_inside_the_share() {
|
||||||
let root = std::env::temp_dir().join(format!("nexus-drive-{}", std::process::id()));
|
let root = std::env::temp_dir().join(format!("nexus-drive-{}", std::process::id()));
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ fn open_error(path: &str, e: &serialport::Error) -> crate::Error {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
#[cfg(unix)]
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
Type=Application
|
Type=Application
|
||||||
Name=Nexus Terminal
|
Name=Nexus Terminal
|
||||||
GenericName=Terminal and Remote Connections
|
GenericName=Terminal and Remote Connections
|
||||||
Comment=SSH, Telnet, RAW TCP, Serial, VNC, IRC and GitHub sessions in one window
|
Comment=SSH, Telnet, RAW TCP, Serial, VNC, RDP, IRC and GitHub sessions in one window
|
||||||
Exec=nexus-terminal
|
Exec=nexus-terminal
|
||||||
Icon=nexus-terminal
|
Icon=nexus-terminal
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=System;TerminalEmulator;Network;RemoteAccess;
|
Categories=System;TerminalEmulator;Network;RemoteAccess;
|
||||||
Keywords=ssh;telnet;serial;sftp;vnc;irc;terminal;
|
Keywords=ssh;telnet;serial;sftp;vnc;rdp;irc;terminal;
|
||||||
StartupWMClass=nexus-terminal
|
StartupWMClass=nexus-terminal
|
||||||
|
|||||||
Executable
+231
@@ -0,0 +1,231 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# Copyright (C) 2026 Andy Dixon
|
||||||
|
#
|
||||||
|
# Publish a Nexus Terminal release as the `nexusterminal` package (.deb, .rpm and Arch) in the
|
||||||
|
# signed apt, dnf and pacman repositories served at https://repo.dixon.cx.
|
||||||
|
#
|
||||||
|
# packaging/repo.sh # the newest tag
|
||||||
|
# packaging/repo.sh v3.0.2 # a given tag
|
||||||
|
#
|
||||||
|
# Unlike the command-line tools' copies of this script, nothing is built here: Nexus is a GUI app
|
||||||
|
# linked against the platform's windowing stack, so the packages wrap the official binaries that
|
||||||
|
# the Release Builds workflow made for the tag (native glibc builds on amd64 and arm64). They're
|
||||||
|
# downloaded from the GitHub release and checked against its SHA256SUMS first.
|
||||||
|
#
|
||||||
|
# The publish half is the same as in ../../../mapsize/packaging/repo.sh and friends: every index
|
||||||
|
# is rebuilt over all packages in the repository, so other projects' packages are kept.
|
||||||
|
#
|
||||||
|
# Needs: gh (or curl), unzip, curl (for the pinned nfpm), docker, gpg, apt-ftparchive, flock.
|
||||||
|
# REPO_DIR where the repository lives (default below)
|
||||||
|
# REPO_GNUPGHOME GnuPG home holding the signing key (default below)
|
||||||
|
# ARCHS package architectures (default: amd64 arm64)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---- project ---------------------------------------------------------------
|
||||||
|
PACKAGE=nexusterminal
|
||||||
|
SUMMARY="Native desktop client for SSH, SFTP, Telnet, Serial, VNC, RDP, IRC and GitHub"
|
||||||
|
HOMEPAGE=https://dixon.cx/nexus
|
||||||
|
GITHUB=andydixon/nexus
|
||||||
|
LICENSE=AGPL-3.0-only
|
||||||
|
ARCHS=${ARCHS:-"amd64 arm64"}
|
||||||
|
# The binaries come from ubuntu-22.04 runners: glibc 2.35. Windowing, GL and audio libraries are
|
||||||
|
# loaded at run time, so they're recommended rather than hard dependencies (any desktop has them).
|
||||||
|
DEB_DEPENDS='"libc6 (>= 2.35)", "libgcc-s1", "zlib1g"'
|
||||||
|
DEB_RECOMMENDS='"libxkbcommon0", "libwayland-client0", "libx11-6", "libxcursor1", "libxrandr2", "libxi6", "libgl1", "libegl1", "libasound2", "xdg-desktop-portal"'
|
||||||
|
RPM_DEPENDS='"glibc >= 2.35", "libgcc", "zlib"'
|
||||||
|
RPM_RECOMMENDS='"libxkbcommon", "libwayland-client", "libX11", "libXcursor", "libXrandr", "libXi", "mesa-libGL", "mesa-libEGL", "alsa-lib", "xdg-desktop-portal"'
|
||||||
|
ARCH_DEPENDS='"glibc", "gcc-libs", "zlib", "libxkbcommon", "libglvnd"'
|
||||||
|
ARCH_OPTDEPENDS='"wayland: Wayland sessions", "libx11: X11 sessions", "libxcursor: X11 sessions", "libxrandr: X11 sessions", "libxi: X11 sessions", "alsa-lib: RDP audio", "xdg-desktop-portal: file dialogs"'
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
REPO_DIR=${REPO_DIR:-/home/andy/domains/repo.dixon.cx/public_html}
|
||||||
|
REPO_GNUPGHOME=${REPO_GNUPGHOME:-$HOME/.config/repo.dixon.cx/gnupg}
|
||||||
|
SUITE=stable
|
||||||
|
COMPONENT=main
|
||||||
|
ARCH_REPO=dixon # the pacman repository name: [dixon]
|
||||||
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
|
||||||
|
say() { echo "repo: $*" >&2; }
|
||||||
|
die() { say "$*"; exit 1; }
|
||||||
|
|
||||||
|
for tool in gh unzip curl docker gpg apt-ftparchive flock sha256sum; do
|
||||||
|
command -v $tool >/dev/null || die "needs $tool"
|
||||||
|
done
|
||||||
|
[[ -d $REPO_DIR ]] || die "$REPO_DIR does not exist"
|
||||||
|
export GNUPGHOME=$REPO_GNUPGHOME
|
||||||
|
KEY=$(gpg --list-secret-keys --with-colons 2>/dev/null | awk -F: '/^fpr/{print $10; exit}')
|
||||||
|
[[ -n $KEY ]] || die "no signing key in $GNUPGHOME"
|
||||||
|
|
||||||
|
cd "$(git -C "$HERE" rev-parse --show-toplevel)"
|
||||||
|
TAG=${1:-$(git describe --tags --abbrev=0)}
|
||||||
|
VERSION=${TAG#v}
|
||||||
|
|
||||||
|
WORK=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
gpg --batch --armor --export-secret-keys "$KEY" >"$WORK/signing.asc"
|
||||||
|
|
||||||
|
# The pinned nfpm, as in the other projects (checksummed download, cached under target/tools).
|
||||||
|
NFPM_VERSION=2.41.1
|
||||||
|
case $(uname -m) in
|
||||||
|
x86_64) nfpm_arch=x86_64 nfpm_sum=b3cf95aa6dabed836d09ad7f0c190a13c74c5b1304db60846f0f702ee407f430 ;;
|
||||||
|
aarch64) nfpm_arch=arm64 nfpm_sum=17350a838c8e2c422c6e573ed379b18424565d2de8a2b1cb1b20211976124eb5 ;;
|
||||||
|
*) die "unsupported host $(uname -m)" ;;
|
||||||
|
esac
|
||||||
|
NFPM_DIR=$PWD/rust/target/tools/nfpm-$NFPM_VERSION
|
||||||
|
if [[ ! -x $NFPM_DIR/nfpm ]]; then
|
||||||
|
mkdir -p "$NFPM_DIR"
|
||||||
|
curl -fsSL -o "$NFPM_DIR/nfpm.tar.gz" "https://github.com/goreleaser/nfpm/releases/download/v$NFPM_VERSION/nfpm_${NFPM_VERSION}_Linux_$nfpm_arch.tar.gz"
|
||||||
|
echo "$nfpm_sum $NFPM_DIR/nfpm.tar.gz" | sha256sum -c --quiet
|
||||||
|
tar -xzf "$NFPM_DIR/nfpm.tar.gz" -C "$NFPM_DIR" nfpm
|
||||||
|
rm -f "$NFPM_DIR/nfpm.tar.gz"
|
||||||
|
fi
|
||||||
|
NFPM=$NFPM_DIR/nfpm
|
||||||
|
|
||||||
|
# ---- fetch and verify the release binaries ---------------------------------
|
||||||
|
say "fetching $TAG from github.com/$GITHUB"
|
||||||
|
mkdir -p "$WORK/dl"
|
||||||
|
patterns=(--pattern SHA256SUMS)
|
||||||
|
for arch in $ARCHS; do patterns+=(--pattern "NexusTerminal-linux-$arch.zip"); done
|
||||||
|
gh release download "$TAG" -R "$GITHUB" -D "$WORK/dl" "${patterns[@]}" || die "release $TAG has no Linux assets yet"
|
||||||
|
(cd "$WORK/dl" && grep -E "NexusTerminal-linux-($(tr ' ' '|' <<<"$ARCHS"))\.zip" SHA256SUMS | sha256sum -c --quiet) || die "checksum mismatch"
|
||||||
|
|
||||||
|
# ---- package -----------------------------------------------------------------
|
||||||
|
say "packaging $PACKAGE $VERSION for $ARCHS"
|
||||||
|
mkdir -p "$WORK/out"
|
||||||
|
for arch in $ARCHS; do
|
||||||
|
src=$WORK/src-$arch
|
||||||
|
mkdir -p "$src"
|
||||||
|
unzip -q "$WORK/dl/NexusTerminal-linux-$arch.zip" -d "$src"
|
||||||
|
[[ -x $src/nexus-terminal ]] || die "no nexus-terminal binary in the $arch zip"
|
||||||
|
cp "$src/nexus-terminal.desktop" "$src/$PACKAGE.desktop"
|
||||||
|
cat >"$WORK/nfpm-$arch.yaml" <<-EOF
|
||||||
|
name: $PACKAGE
|
||||||
|
arch: $arch
|
||||||
|
platform: linux
|
||||||
|
version: $VERSION
|
||||||
|
release: 1
|
||||||
|
section: net
|
||||||
|
maintainer: Andy Dixon <andy@dixon.cx>
|
||||||
|
description: |
|
||||||
|
$SUMMARY.
|
||||||
|
Tabs, split panes, SFTP, port forwards, jump hosts, session recording and an
|
||||||
|
encrypted credential vault, in one native app written in Rust.
|
||||||
|
homepage: $HOMEPAGE
|
||||||
|
license: $LICENSE
|
||||||
|
contents:
|
||||||
|
- src: $src/nexus-terminal
|
||||||
|
dst: /usr/bin/nexus-terminal
|
||||||
|
file_info: {mode: 0755}
|
||||||
|
- src: /usr/bin/nexus-terminal
|
||||||
|
dst: /usr/bin/$PACKAGE
|
||||||
|
type: symlink
|
||||||
|
- src: $src/$PACKAGE.desktop
|
||||||
|
dst: /usr/share/applications/$PACKAGE.desktop
|
||||||
|
file_info: {mode: 0644}
|
||||||
|
- src: $src/app/assets/icon/icon-256.png
|
||||||
|
dst: /usr/share/icons/hicolor/256x256/apps/nexus-terminal.png
|
||||||
|
file_info: {mode: 0644}
|
||||||
|
- src: $src/app/assets/icon/icon-512.png
|
||||||
|
dst: /usr/share/icons/hicolor/512x512/apps/nexus-terminal.png
|
||||||
|
file_info: {mode: 0644}
|
||||||
|
- src: $src/LICENSE
|
||||||
|
dst: /usr/share/licenses/$PACKAGE/LICENSE
|
||||||
|
file_info: {mode: 0644}
|
||||||
|
- src: $PWD/README.md
|
||||||
|
dst: /usr/share/doc/$PACKAGE/README.md
|
||||||
|
file_info: {mode: 0644}
|
||||||
|
overrides:
|
||||||
|
deb:
|
||||||
|
depends: [$DEB_DEPENDS]
|
||||||
|
recommends: [$DEB_RECOMMENDS]
|
||||||
|
rpm:
|
||||||
|
depends: [$RPM_DEPENDS]
|
||||||
|
recommends: [$RPM_RECOMMENDS]
|
||||||
|
archlinux:
|
||||||
|
depends: [$ARCH_DEPENDS]
|
||||||
|
archlinux:
|
||||||
|
packager: Andy Dixon <andy@dixon.cx>
|
||||||
|
optdepends: [$ARCH_OPTDEPENDS]
|
||||||
|
rpm:
|
||||||
|
signature:
|
||||||
|
key_file: $WORK/signing.asc
|
||||||
|
EOF
|
||||||
|
for fmt in deb rpm archlinux; do
|
||||||
|
"$NFPM" pkg -f "$WORK/nfpm-$arch.yaml" -p $fmt -t "$WORK/out/" >/dev/null
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- publish (shared with the other projects' repo.sh) -----------------------
|
||||||
|
exec 9>"$REPO_DIR/../.publish.lock"
|
||||||
|
flock 9
|
||||||
|
say "publishing to $REPO_DIR"
|
||||||
|
gpg --batch --armor --export "$KEY" >"$REPO_DIR/dixon.asc"
|
||||||
|
gpg --batch --export "$KEY" >"$REPO_DIR/dixon.gpg"
|
||||||
|
sign() { gpg --batch --yes --local-user "$KEY" "$@"; }
|
||||||
|
|
||||||
|
pool=$REPO_DIR/apt/pool/$COMPONENT/${PACKAGE:0:1}/$PACKAGE
|
||||||
|
mkdir -p "$pool"
|
||||||
|
cp "$WORK"/out/*.deb "$pool/"
|
||||||
|
(
|
||||||
|
cd "$REPO_DIR/apt"
|
||||||
|
dists=dists/.$SUITE.new
|
||||||
|
rm -rf "$dists"
|
||||||
|
debarchs=$(find pool -name '*.deb' | sed -E 's/.*_([^_]+)\.deb$/\1/' | sort -u | tr '\n' ' ')
|
||||||
|
for a in $debarchs; do
|
||||||
|
d=$dists/$COMPONENT/binary-$a
|
||||||
|
mkdir -p "$d"
|
||||||
|
apt-ftparchive --arch "$a" packages pool >"$d/Packages"
|
||||||
|
gzip -9kn "$d/Packages"
|
||||||
|
done
|
||||||
|
apt-ftparchive \
|
||||||
|
-o APT::FTPArchive::Release::Origin=repo.dixon.cx \
|
||||||
|
-o APT::FTPArchive::Release::Label=repo.dixon.cx \
|
||||||
|
-o APT::FTPArchive::Release::Suite=$SUITE \
|
||||||
|
-o APT::FTPArchive::Release::Codename=$SUITE \
|
||||||
|
-o APT::FTPArchive::Release::Components=$COMPONENT \
|
||||||
|
-o "APT::FTPArchive::Release::Architectures=${debarchs% }" \
|
||||||
|
release "$dists" >"$WORK/Release"
|
||||||
|
mv "$WORK/Release" "$dists/Release"
|
||||||
|
sign --clearsign -o "$dists/InRelease" "$dists/Release"
|
||||||
|
sign --armor --detach-sign -o "$dists/Release.gpg" "$dists/Release"
|
||||||
|
rm -rf "dists/.$SUITE.old"
|
||||||
|
[[ -d dists/$SUITE ]] && mv "dists/$SUITE" "dists/.$SUITE.old"
|
||||||
|
mv "$dists" "dists/$SUITE"
|
||||||
|
rm -rf "dists/.$SUITE.old"
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$REPO_DIR/rpm"
|
||||||
|
cp "$WORK"/out/*.rpm "$REPO_DIR/rpm/"
|
||||||
|
docker image inspect repo-tools:fedora >/dev/null 2>&1 ||
|
||||||
|
docker build -q -t repo-tools:fedora - >/dev/null <<-'EOF'
|
||||||
|
FROM fedora:42
|
||||||
|
RUN dnf -y install createrepo_c && dnf clean all
|
||||||
|
EOF
|
||||||
|
docker run --rm --user "$(id -u):$(id -g)" -v "$REPO_DIR/rpm:/repo" repo-tools:fedora \
|
||||||
|
createrepo_c --quiet --update --general-compress-type=gz /repo
|
||||||
|
sign --armor --detach-sign -o "$REPO_DIR/rpm/repodata/repomd.xml.asc" "$REPO_DIR/rpm/repodata/repomd.xml"
|
||||||
|
|
||||||
|
added=
|
||||||
|
for pkg in "$WORK"/out/*.pkg.tar.zst; do
|
||||||
|
a=$(basename "$pkg" .pkg.tar.zst)
|
||||||
|
a=${a##*-}
|
||||||
|
mkdir -p "$REPO_DIR/arch/$a"
|
||||||
|
cp "$pkg" "$REPO_DIR/arch/$a/"
|
||||||
|
sign --detach-sign -o "$REPO_DIR/arch/$a/$(basename "$pkg").sig" "$pkg"
|
||||||
|
added+=" $a"
|
||||||
|
done
|
||||||
|
for a in $(tr ' ' '\n' <<<"$added" | sort -u); do
|
||||||
|
dir=$REPO_DIR/arch/$a
|
||||||
|
rm -f "$dir/$ARCH_REPO".{db,files}{,.tar.gz}{,.sig}
|
||||||
|
docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp -v "$dir:/repo" -w /repo archlinux:latest \
|
||||||
|
sh -c "repo-add --quiet --prevent-downgrade $ARCH_REPO.db.tar.gz *.pkg.tar.zst" 2>&1 |
|
||||||
|
{ grep -v "newer version\|^$" >&2 || true; }
|
||||||
|
for db in $ARCH_REPO.db $ARCH_REPO.files; do
|
||||||
|
sign --detach-sign -o "$dir/$db.tar.gz.sig" "$dir/$db.tar.gz"
|
||||||
|
ln -sfn "$db.tar.gz.sig" "$dir/$db.sig"
|
||||||
|
done
|
||||||
|
rm -f "$dir"/*.old "$dir"/*.old.sig
|
||||||
|
done
|
||||||
|
|
||||||
|
say "published $PACKAGE $VERSION: $(cd "$WORK/out" && ls | wc -l) packages"
|
||||||
Reference in New Issue
Block a user