blob: 34eebd729544a02e470635f97b1cf1a873d72e10 [file]
#!/usr/bin/env bash
# Casbin Gateway one-step install for Linux and macOS.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/apache/casbin-gateway/master/scripts/install.sh | bash
#
# This downloads the nightly build, which is an automated build of master and
# not an official release. Use it for testing and development only.
#
# Optional environment variables:
# INSTALL_DIR where the executable and its data live
# (default: $HOME/.local/share/casbin-gateway)
# BIN_DIR where the "casbin-gateway" command is placed
# (default: $HOME/.local/bin)
# NO_START set to any value to install without starting Gateway
# NO_AUTOSTART set to any value to skip the login-time startup entry
# NO_SHORTCUT set to any value to skip the macOS bundle or the desktop entry
set -euo pipefail
REPO="apache/casbin-gateway"
TAG="nightly"
BASENAME="casbin-gateway-nightly"
INSTALL_DIR="${INSTALL_DIR:-${HOME}/.local/share/casbin-gateway}"
BIN_DIR="${BIN_DIR:-${HOME}/.local/bin}"
NO_START="${NO_START:-}"
NO_AUTOSTART="${NO_AUTOSTART:-}"
NO_SHORTCUT="${NO_SHORTCUT:-}"
info() { printf '%s\n' "$*"; }
die() { printf 'casbin-gateway: %s\n' "$*" >&2; exit 1; }
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}
need_cmd curl
need_cmd tar
# ── pick the archive for this machine ─────────────────────────────────────────
os="$(uname -s)"
case "${os}" in
Linux) osName="linux" ;;
Darwin) osName="darwin" ;;
*) die "unsupported operating system \"${os}\", build from source instead: https://github.com/${REPO}" ;;
esac
# Only x86_64 archives are published. macOS runs them under Rosetta 2, so an
# Apple Silicon machine still gets a working install; on Linux arm64 there is
# nothing to fall back to.
arch="$(uname -m)"
case "${arch}" in
x86_64|amd64) archName="x86_64" ;;
aarch64|arm64)
[[ "${osName}" == "darwin" ]] || die "unsupported architecture \"${arch}\", build from source instead: https://github.com/${REPO}"
archName="x86_64"
info "No arm64 build is published, installing the x86_64 one to run under Rosetta 2"
;;
*) die "unsupported architecture \"${arch}\", build from source instead: https://github.com/${REPO}" ;;
esac
archive="${BASENAME}-${osName}-${archName}.tar.gz"
url="https://github.com/${REPO}/releases/download/${TAG}/${archive}"
# ── download ──────────────────────────────────────────────────────────────────
tmpDir="$(mktemp -d)"
trap 'rm -rf "${tmpDir}"' EXIT
info "Downloading ${url}"
curl -fsSL -o "${tmpDir}/${archive}" "${url}"
# ── install ───────────────────────────────────────────────────────────────────
info "Installing to ${INSTALL_DIR}"
mkdir -p "${INSTALL_DIR}" || die "cannot create ${INSTALL_DIR}, set INSTALL_DIR to a directory you can write to"
# Unpacking inside INSTALL_DIR keeps the final move on one filesystem, so it is
# a rename: an already running Gateway holds the old executable open, and
# writing over it in place would fail with "text file busy".
stage="${INSTALL_DIR}/.install"
rm -rf "${stage}"
mkdir -p "${stage}"
tar -xzf "${tmpDir}/${archive}" -C "${stage}" --strip-components=1
for executable in casbin-gateway casbin-gateway-desktop; do
mv -f "${stage}/${executable}" "${INSTALL_DIR}/${executable}"
chmod 755 "${INSTALL_DIR}/${executable}"
done
for legalFile in LICENSE NOTICE DISCLAIMER; do
mv -f "${stage}/${legalFile}" "${INSTALL_DIR}/${legalFile}"
done
rm -rf "${stage}"
desktopExe="${INSTALL_DIR}/casbin-gateway-desktop"
iconPath="$("${desktopExe}" icon)"
# ── put a "casbin-gateway" command on PATH ────────────────────────────────────
# Gateway keeps its database, logs and temporary files in the working
# directory, so the command is a wrapper that always starts it in INSTALL_DIR.
# Without it, running "casbin-gateway" from somewhere else would quietly start
# a second, empty installation.
if [[ "${BIN_DIR}" == "${INSTALL_DIR}" ]]; then
info "BIN_DIR is the install directory, so no wrapper was created"
else
mkdir -p "${BIN_DIR}" || die "cannot create ${BIN_DIR}, set BIN_DIR to a directory you can write to"
cat > "${BIN_DIR}/casbin-gateway" <<EOF
#!/usr/bin/env bash
# Written by the Casbin Gateway installer. Gateway reads and writes ./data,
# ./logs and ./tmp, so it always has to start in its own directory.
cd "${INSTALL_DIR}" || exit 1
exec "${INSTALL_DIR}/casbin-gateway" "\$@"
EOF
chmod 755 "${BIN_DIR}/casbin-gateway"
case ":${PATH}:" in
*":${BIN_DIR}:"*) ;;
*)
shellRc=""
case "${SHELL:-}" in
*/zsh) shellRc="${HOME}/.zshrc" ;;
*/bash) shellRc="${HOME}/.bashrc" ;;
esac
if [[ -n "${shellRc}" ]]; then
printf '\nexport PATH="%s:$PATH"\n' "${BIN_DIR}" >> "${shellRc}"
info "Added ${BIN_DIR} to PATH in ${shellRc}, which takes effect in your next shell"
else
info "${BIN_DIR} is not on your PATH, add it to run Gateway by name"
fi
;;
esac
fi
# ── the desktop application ───────────────────────────────────────────────────
# A macOS bundle and a Linux desktop entry, both pointing at the launcher rather
# than the server: it is what shows the window, and it starts the server itself.
appNote=""
install_macos_app() {
appDir="${HOME}/Applications/Casbin Gateway.app"
rm -rf "${appDir}"
mkdir -p "${appDir}/Contents/MacOS" "${appDir}/Contents/Resources" || return
# The launcher is copied in rather than symlinked or wrapped: a bundle whose
# executable runs from outside it gets neither the Dock icon nor the name.
cp "${desktopExe}" "${appDir}/Contents/MacOS/casbin-gateway-desktop"
chmod 755 "${appDir}/Contents/MacOS/casbin-gateway-desktop"
cp "${iconPath}" "${appDir}/Contents/Resources/appicon.icns"
# Which is also why the data directory is recorded here: the copy inside the
# bundle cannot find it by looking next to itself.
printf '%s\n' "${INSTALL_DIR}" > "${appDir}/Contents/Resources/home"
cat > "${appDir}/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>Casbin Gateway</string>
<key>CFBundleDisplayName</key><string>Casbin Gateway</string>
<key>CFBundleIdentifier</key><string>org.apache.casbin-gateway</string>
<key>CFBundleExecutable</key><string>casbin-gateway-desktop</string>
<key>CFBundleIconFile</key><string>appicon</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>NSHighResolutionCapable</key><true/>
</dict>
</plist>
EOF
# The download is not signed or notarized. Nothing curl writes is
# quarantined, so this only matters for an archive that arrived some other
# way, and an ad-hoc signature is the most this can do without a developer
# certificate. Both are best-effort.
xattr -dr com.apple.quarantine "${appDir}" >/dev/null 2>&1 || true
command -v codesign >/dev/null 2>&1 && codesign --force --deep --sign - "${appDir}" >/dev/null 2>&1
# Finder caches the bundle by path, so a reinstall over the old one keeps
# showing the old icon until it is told the directory changed.
touch "${appDir}"
desktopExe="${appDir}/Contents/MacOS/casbin-gateway-desktop"
appNote="${appDir}"
}
install_linux_app() {
entryDir="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
iconDir="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor/512x512/apps"
mkdir -p "${entryDir}" "${iconDir}" || return
cp "${iconPath}" "${iconDir}/org.apache.casbin-gateway.png"
entry="${entryDir}/org.apache.casbin-gateway.desktop"
cat > "${entry}" <<EOF
[Desktop Entry]
Type=Application
Name=Casbin Gateway
Comment=Local gateway for AI agents
Exec=${desktopExe}
Path=${INSTALL_DIR}
Icon=org.apache.casbin-gateway
Terminal=false
Categories=Development;Network;
StartupWMClass=casbin-gateway-desktop
EOF
chmod 644 "${entry}"
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "${entryDir}" >/dev/null 2>&1
# Most desktops only show an icon on ~/Desktop if the file is executable and
# marked trusted; the chmod is the half that is portable.
if [[ -d "${HOME}/Desktop" ]]; then
cp "${entry}" "${HOME}/Desktop/org.apache.casbin-gateway.desktop"
chmod 755 "${HOME}/Desktop/org.apache.casbin-gateway.desktop"
command -v gio >/dev/null 2>&1 && gio set "${HOME}/Desktop/org.apache.casbin-gateway.desktop" metadata::trusted true >/dev/null 2>&1
fi
appNote="${entry}"
}
if [[ -z "${NO_SHORTCUT}" ]]; then
if [[ "${osName}" == "darwin" ]]; then
install_macos_app
else
install_linux_app
fi
fi
# ── start at login ────────────────────────────────────────────────────────────
# The launcher owns this entry so that the tray's "Start at Login" checkbox and
# the installer are never out of step. Older installs started the server on its
# own; those entries would now bring up a second copy.
rm -f "${HOME}/Library/LaunchAgents/org.apache.casbin-gateway.plist"
if command -v systemctl >/dev/null 2>&1 && [[ -f "${HOME}/.config/systemd/user/casbin-gateway.service" ]]; then
systemctl --user disable --now casbin-gateway.service >/dev/null 2>&1 || true
rm -f "${HOME}/.config/systemd/user/casbin-gateway.service"
systemctl --user daemon-reload >/dev/null 2>&1 || true
fi
autostartNote=""
if [[ -z "${NO_AUTOSTART}" ]]; then
if "${desktopExe}" autostart on; then
autostartNote="yes"
fi
fi
info ""
info "Casbin Gateway is installed in ${INSTALL_DIR}"
info "Its database, logs and temporary files stay in that directory."
info "It serves this machine only, and signs you in there as admin without a password."
info "Closing its window leaves it running in the tray; quit it from there."
info "Without the window: \"casbin-gateway start\", \"casbin-gateway stop\", \"casbin-gateway status\"."
if [[ -n "${appNote}" ]]; then
info "The application is ${appNote}"
fi
if [[ -n "${autostartNote}" ]]; then
info "It will start when you log in; turn that off from the tray menu."
fi
info ""
if [[ -n "${NO_START}" ]]; then
info "Start it from the Casbin Gateway application, or with: casbin-gateway start"
exit 0
fi
# The launcher forks the server and the window, so installing does not occupy
# this terminal beyond the startup it waits for.
cd "${INSTALL_DIR}"
"${desktopExe}" >/dev/null 2>&1 &
disown 2>/dev/null || true