quick_search/build.sh
= cf40196546
Some checks failed
CI / linux (push) Successful in 2m49s
CI / windows-cross (push) Failing after 1m20s
CI / release (push) Has been skipped
Added index-preserving pruning. Added windows installer to CI, added automated GUI screenshot generation for documentation.
2026-08-04 23:33:28 -04:00

286 lines
11 KiB
Bash
Executable file

#!/usr/bin/env sh
#
# Set up the build environment, build QuickSearch, and launch the GUI. On Unix
# the same binary doubles as the terminal search tool:
# ./target/release/quicksearch --help
# (On Windows that role belongs to quicksearch-cli.exe — see build.bat.)
#
# ./build.sh install what is missing, build release, launch
# ./build.sh --no-run stop after the build
# ./build.sh --check report dependency status; install and build nothing
# ./build.sh --installer build the Windows installer instead of running
# ./build.sh -- <args> pass everything after -- to the launched binary
#
# Anything the script does not recognise ends its own option parsing, so a bare
# search term reaches the binary; use -- for arguments that look like flags.
#
# On a fresh Linux machine this installs the C toolchain, Perl and pkg-config
# with the distribution's package manager (via sudo), then the Rust toolchain
# with rustup. Every stage is skipped when what it provides is already present,
# so the everyday run costs one `cargo build`.
# Other Unixes only get the Rust stage — see the README for their toolchains.
#
# --installer adds NSIS and the mingw-w64 cross toolchain to that list and hands
# off to packaging/build-installer.sh, which cross-compiles and produces
# dist/quicksearch-<version>-windows-x86_64-setup.exe. Arguments after -- go to
# that script, so `./build.sh --installer -- --no-strip` works. It is opt-in
# because neither tool has anything to do with building or running QuickSearch
# here: an ordinary ./build.sh should not install a Windows installer compiler.
set -e
# Not `dirname`: a script whose job is to install missing tools should lean on
# as few of them as possible. $0 has no slash when the script is found on PATH.
case "$0" in
*/*) script_dir="${0%/*}" ;;
*) script_dir=. ;;
esac
REPO_ROOT="$(cd -- "$script_dir" && pwd)"
die() { printf 'build: %s\n' "$*" >&2; exit 1; }
say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
need_cmd() { command -v "$1" >/dev/null 2>&1; }
do_run=1
mode=run
want_installer=0
while [ $# -gt 0 ]; do
case "$1" in
--no-run) do_run=0 ;;
--check) mode=check ;;
# There is no Linux binary to launch at the end of an installer build.
--installer) want_installer=1; do_run=0 ;;
# Print the header comment block, however long it grows.
-h|--help) awk 'NR > 1 { if ($0 !~ /^#/) exit; sub(/^# ?/, ""); print }' "$0"; exit 0 ;;
--) shift; break ;;
# Not ours: it and everything after it belongs to the binary.
*) break ;;
esac
shift
done
# ------------------------------------------------------------ toolchain ----
# cargo may be installed but absent from this shell's PATH, which is the normal
# state right after rustup runs: it appends to the shell profile, and
# ~/.cargo/env is what it writes for processes that cannot wait for a new login.
have_cargo() {
if need_cmd cargo; then return 0; fi
if [ -r "$HOME/.cargo/env" ]; then
. "$HOME/.cargo/env"
if need_cmd cargo; then return 0; fi
fi
return 1
}
ensure_rust() {
if have_cargo; then return 0; fi
need_cmd curl || die "curl is needed to fetch rustup"
say "Installing the Rust toolchain (rustup, into ~/.rustup and ~/.cargo)"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain stable
if [ -r "$HOME/.cargo/env" ]; then . "$HOME/.cargo/env"; fi
need_cmd cargo || die "rustup finished but cargo is still not on PATH"
}
# -------------------------------------------------------- system packages ---
# Sets $missing to the space-separated tools that are absent. Probing for the
# command rather than for a package keeps this identical across distributions
# and honours toolchains installed outside the package manager.
#
# Perl is not optional despite never being named in the source: rusqlite's
# bundled-sqlcipher-vendored-openssl feature builds OpenSSL, whose Configure is
# a Perl script. Without it the failure comes hundreds of crates into the build.
check_deps() {
missing=''
if ! need_cmd cc && ! need_cmd gcc && ! need_cmd clang; then missing="$missing cc"; fi
need_cmd make || missing="$missing make"
need_cmd perl || missing="$missing perl"
# pkg-config is how x11-dl and wayland-sys locate the display libraries.
# No -dev packages for those libraries are needed: winit dlopens the whole
# display stack at run time (xkbcommon-dl has no build script at all, and
# x11-dl's records a libdir of None when a .pc file is absent), so headers
# never enter the build.
need_cmd pkg-config || missing="$missing pkg-config"
# curl is a build dependency only while rustup still has to be downloaded.
if ! have_cargo && ! need_cmd curl; then missing="$missing curl"; fi
# Only for --installer: makensis compiles the .nsi into the installer, and
# the mingw-w64 gcc is the linker and the C compiler for the bundled
# SQLCipher, OpenSSL and zstd sources on the Windows target. It also brings
# in the cross binutils, which is where the windres the GUI's build script
# uses to compile the .exe version resource comes from. The Rust side
# needs nothing extra — rust-toolchain.toml already lists the target, so
# rustup installs it with the pinned toolchain.
if [ "$want_installer" = 1 ]; then
need_cmd makensis || missing="$missing makensis"
need_cmd x86_64-w64-mingw32-gcc || missing="$missing x86_64-w64-mingw32-gcc"
fi
}
# The tool -> package mapping, per package manager. Packages named twice (a
# single build-essential covers both cc and make) are deduplicated by the caller.
# Printing nothing means "this distribution has no package for it in its default
# repositories", which the caller reports rather than guessing a name; the two
# --installer tools are the only ones where that happens.
packages_for() {
case "$1:$2" in
apt-get:cc|apt-get:make) echo build-essential ;;
apt-get:makensis) echo nsis ;;
apt-get:x86_64-w64-mingw32-gcc) echo gcc-mingw-w64-x86-64 ;;
apt-get:*) echo "$2" ;;
dnf:cc) echo gcc gcc-c++ ;;
dnf:pkg-config) echo pkgconf-pkg-config ;;
# Fedora ships NSIS as part of its mingw stack, so the name looks
# cross-ish; makensis in it is a native Linux binary all the same.
dnf:makensis) echo mingw32-nsis ;;
dnf:x86_64-w64-mingw32-gcc) echo mingw64-gcc ;;
dnf:*) echo "$2" ;;
pacman:cc|pacman:make) echo base-devel ;;
pacman:pkg-config) echo pkgconf ;;
pacman:makensis) echo nsis ;;
pacman:x86_64-w64-mingw32-gcc) echo mingw-w64-gcc ;;
pacman:*) echo "$2" ;;
zypper:cc) echo gcc gcc-c++ ;;
# Neither NSIS nor the mingw toolchain is in the openSUSE distribution
# repositories — both live in the windows:mingw OBS project, which is a
# repository the user has to add and not something to do behind their
# back. Hence no mapping.
zypper:makensis|zypper:x86_64-w64-mingw32-gcc) ;;
zypper:*) echo "$2" ;;
esac
}
# Sets $sudo_cmd, and returns non-zero when the packages cannot be installed
# from here. $sudo_cmd is set either way, so the commands can still be printed
# for the user to run as root.
resolve_sudo() {
if [ "$(id -u)" = 0 ]; then sudo_cmd=''; return 0; fi
sudo_cmd=sudo
need_cmd sudo
}
# Run or print one package-manager command. Both paths go through here so what
# --check reports cannot drift from what the install stage actually does.
pm_exec() {
if [ "$mode" = run ] && [ "$can_install" = 1 ]; then
say "$*"
"$@"
else
printf ' %s\n' "$*"
fi
}
# $sudo_cmd and $pkgs are deliberately unquoted: an empty sudo_cmd has to
# disappear rather than become an empty argument, and pkgs has to split.
pm_commands() {
case "$pm" in
apt-get)
pm_exec $sudo_cmd env DEBIAN_FRONTEND=noninteractive apt-get update
pm_exec $sudo_cmd env DEBIAN_FRONTEND=noninteractive apt-get install -y $pkgs
;;
dnf) pm_exec $sudo_cmd dnf install -y $pkgs ;;
pacman) pm_exec $sudo_cmd pacman -S --needed --noconfirm $pkgs ;;
zypper) pm_exec $sudo_cmd zypper --non-interactive install $pkgs ;;
esac
}
ensure_system_deps() {
check_deps
[ -n "$missing" ] || return 0
pm=''
for candidate in apt-get dnf pacman zypper; do
if need_cmd "$candidate"; then pm="$candidate"; break; fi
done
[ -n "$pm" ] || die "missing build dependencies:$missing (no apt-get, dnf, pacman or zypper here — install them with your distribution's tools)"
pkgs=''
unmapped=''
for tool in $missing; do
mapped="$(packages_for "$pm" "$tool")"
if [ -n "$mapped" ]; then
pkgs="$pkgs $mapped"
else
unmapped="$unmapped $tool"
fi
done
pkgs="$(printf '%s\n' $pkgs | sort -u | tr '\n' ' ')"
if [ -n "$unmapped" ]; then
say "no package for$unmapped in this distribution's repositories — install it yourself, see the README"
# Nothing left to install means there is nothing to say beyond that.
if [ -z "$pkgs" ]; then
if [ "$mode" = run ]; then die "cannot continue without$unmapped"; fi
return 0
fi
fi
can_install=1
resolve_sudo || can_install=0
if [ "$mode" != run ]; then
printf 'Install with:\n'
pm_commands
return 0
fi
if [ "$can_install" = 0 ]; then
printf 'build: missing build dependencies:%s\nRun these as root, then try again:\n' "$missing" >&2
pm_commands >&2
exit 1
fi
say "Installing build dependencies:$missing"
pm_commands
check_deps
[ -z "$missing" ] || die "still missing after installing packages:$missing"
}
# ------------------------------------------------------------- stages ------
case "$(uname -s)" in
Linux)
ensure_system_deps
;;
*)
# Only Linux package managers are handled; elsewhere the C toolchain is
# a manual step (on macOS: xcode-select --install). See the README.
check_deps
[ -z "$missing" ] || say "not Linux — install these yourself, see the README:$missing"
;;
esac
if [ "$mode" = check ]; then
if have_cargo; then
printf ' %s\n' "cargo: $(cargo --version)"
else
printf ' %s\n' "cargo: MISSING (rustup would install it)"
fi
[ -z "$missing" ] || exit 1
say "Ready to build"
exit 0
fi
ensure_rust
cd "$REPO_ROOT"
# The installer build has its own cargo invocation — a different target, its own
# staging and makensis - so there is nothing for the native build below to
# contribute, and no Linux binary to launch afterwards.
if [ "$want_installer" = 1 ]; then
exec "$REPO_ROOT/packaging/build-installer.sh" "$@"
fi
cargo build --release -p quicksearch-gui
if [ "$do_run" = 0 ]; then
say "Built target/release/quicksearch and target/release/quicksearch-cli"
exit 0
fi
exec "$REPO_ROOT/target/release/quicksearch" "$@"