chore: padroniza arquitetura Ciberfauna, elevação root na abertura e CI multi-arch no runner ARM
CI / Formatação e clippy (push) Successful in 20s
CI / Auditoria de dependências (push) Successful in 9s
CI / Compila o app desktop (Linux) (push) Successful in 1m55s
Release / Cria a release (push) Successful in 5s
CI / Testes (Linux) (push) Successful in 4m25s
Release / CLI (linux-aarch64) (push) Successful in 2m18s
Release / CLI (linux-x86_64) (push) Successful in 2m51s
Release / App desktop (linux-aarch64) (push) Successful in 9m19s
Release / App desktop (linux-x86_64) (push) Failing after 11m11s
Release / App desktop (windows) (push) Successful in 10m27s
Release / Monta o latest.json (push) Skipped

This commit is contained in:
Lucas Rayan committed 2026-10-30 17:38:54 -03:00
1 parent 009c3d61de
commit 2786d28e7e
98 files changed
+10594 -372

No files matched your search

+4 -4
View File
@@ -16,15 +16,15 @@ mkdir -p "$cache/partial"
trap 'sudo chown -R "$(id -u):$(id -g)" "$cache" 2>/dev/null || true; rm -f "$cache/lock"' EXIT
opts=(-o "Dir::Cache::archives=$cache" -y)
# 1) com .deb em cache: instala direto, sem nem atualizar o índice do apt
if ls "$cache"/*.deb >/dev/null 2>&1 && timeout 300 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then
# 1) com .deb em cache e índice presente: instala direto, sem nem atualizar o índice do apt
if [ -z "$(dpkg --print-foreign-architectures)" ] && ls "$cache"/*.deb >/dev/null 2>&1 && ls /var/lib/apt/lists/*Packages* >/dev/null 2>&1 && timeout 300 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then
echo "instalado a partir do cache"
exit 0
fi
# 2) sem cache (ou incompleto): atualiza e instala, até 3 tentativas com tempo limite
# 2) sem cache (ou multiarch / incompleto): atualiza e instala, até 3 tentativas com tempo limite
for tentativa in 1 2 3; do
if timeout 240 sudo apt-get update && timeout 600 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then
if timeout 240 sudo apt-get update && { [ -z "$(dpkg --print-foreign-architectures)" ] || timeout 600 sudo apt-get "${opts[@]}" upgrade; } && timeout 600 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then
echo "instalado (tentativa $tentativa)"
exit 0
fi
+84 -4
View File
@@ -17,7 +17,7 @@
set -euo pipefail
cd "$(dirname "$0")/.."
platform=${1:?uso: build-gui.sh <linux|windows|windows-docker>}
platform=${1:?uso: build-gui.sh <linux|linux-x86_64|linux-aarch64|windows|windows-docker>}
tauri() { npx --yes @tauri-apps/cli@2 "$@"; }
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ] && [ -f "$HOME/.tauri/guara-updater.key" ]; then
@@ -45,22 +45,102 @@ if [ "$platform" = windows-docker ]; then
chown -R "$HOST_UID:$HOST_GID" dist-gui apps/gui/src-tauri/target 2>/dev/null || true'
fi
rm -rf dist-gui
mkdir -p dist-gui
cd apps/gui/src-tauri
[ -f icons/icon.ico ] || tauri icon icons/source.png -o icons
tauri_blob_cache="$HOME/.cache/tauri"
if [ -d /opt/hostedtoolcache ] && [ -w /opt/hostedtoolcache ]; then
tauri_blob_cache="/opt/hostedtoolcache/tauri-tools"
fi
mkdir -p "$tauri_blob_cache" "$HOME/.cache/tauri"
fetch_cached_tool() {
local name=$1 url=$2 patch_elf=${3:-0}
local cached="$tauri_blob_cache/$name"
if [ ! -s "$cached" ]; then
local tmp="$cached.tmp.$$"
curl -fsSL "$url" -o "$tmp"
if [ "$patch_elf" = 1 ]; then
dd if=/dev/zero of="$tmp" bs=1 count=3 seek=8 conv=notrunc 2>/dev/null
fi
chmod +x "$tmp"
mv -f "$tmp" "$cached"
fi
if [ "$cached" != "$HOME/.cache/tauri/$name" ]; then
cp -f "$cached" "$HOME/.cache/tauri/$name"
chmod +x "$HOME/.cache/tauri/$name"
fi
}
case "$platform" in
linux)
tauri build --bundles appimage,deb
linux|linux-aarch64)
bundle="${CARGO_TARGET_DIR:-target}/release/bundle"
rm -rf "$bundle/appimage" "$bundle/deb"
tauri build --bundles appimage,deb
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
cp "$bundle"/appimage/*.AppImage.sig ../../../dist-gui/ 2>/dev/null || true
;;
linux-x86_64)
if [ "$(uname -m)" = x86_64 ]; then
bundle="${CARGO_TARGET_DIR:-target}/release/bundle"
rm -rf "$bundle/appimage" "$bundle/deb"
tauri build --bundles appimage,deb
else
rustup target add x86_64-unknown-linux-gnu >/dev/null
mkdir -p /tmp/x86_64-cross-tools "$HOME/.cache/tauri"
cat >/tmp/x86_64-cross-tools/ldd <<'EOF'
#!/usr/bin/env bash
exec qemu-x86_64 /lib64/ld-linux-x86-64.so.2 --list "$@"
EOF
chmod +x /tmp/x86_64-cross-tools/ldd
ln -sf /usr/bin/x86_64-linux-gnu-strip /tmp/x86_64-cross-tools/strip
for tool_rel in glib-2.0/gio-querymodules gdk-pixbuf-2.0/gdk-pixbuf-query-loaders libgtk-3-0t64/gtk-query-immodules-3.0 libgtk-3-0/gtk-query-immodules-3.0; do
if [ -x "/usr/lib/x86_64-linux-gnu/$tool_rel" ]; then
tname=$(basename "$tool_rel")
printf '#!/usr/bin/env bash\nexec qemu-x86_64 "/usr/lib/x86_64-linux-gnu/%s" "$@"\n' "$tool_rel" >"/tmp/x86_64-cross-tools/$tname"
chmod +x "/tmp/x86_64-cross-tools/$tname"
fi
done
fetch_cached_tool "linuxdeploy-07333c6-aarch64.AppImage" \
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy-07333c6/linuxdeploy-aarch64.AppImage" 1
fetch_cached_tool "linuxdeploy-plugin-appimage.AppImage" \
"https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-aarch64.AppImage" 1
fetch_cached_tool "runtime-x86_64" \
"https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64" 0
cat >"$HOME/.cache/tauri/linuxdeploy-07333c6-x86_64.AppImage" <<'EOF'
#!/usr/bin/env bash
export ARCH=x86_64
export NO_STRIP=true
export APPIMAGE_EXTRACT_AND_RUN=1
export LDAI_RUNTIME_FILE="$HOME/.cache/tauri/runtime-x86_64"
export LD_GTK_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu
export DEB_HOST_MULTIARCH=x86_64-linux-gnu
export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_LIBDIR=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig
export PATH="/tmp/x86_64-cross-tools:$PATH"
exec "$HOME/.cache/tauri/linuxdeploy-07333c6-aarch64.AppImage" "$@"
EOF
chmod +x "$HOME/.cache/tauri/linuxdeploy-07333c6-x86_64.AppImage"
export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_LIBDIR=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
export CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
export CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++
bundle="${CARGO_TARGET_DIR:-target}/x86_64-unknown-linux-gnu/release/bundle"
rm -rf "$bundle/appimage" "$bundle/deb"
tauri build --target x86_64-unknown-linux-gnu --bundles appimage,deb
fi
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
cp "$bundle"/appimage/*.AppImage.sig ../../../dist-gui/ 2>/dev/null || true
;;
windows)
rustup target add x86_64-pc-windows-msvc >/dev/null
tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
bundle="${CARGO_TARGET_DIR:-target}/x86_64-pc-windows-msvc/release/bundle/nsis"
rm -rf "$bundle"
tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
cp "$bundle"/*-setup.exe ../../../dist-gui/
cp "$bundle"/*-setup.exe.sig ../../../dist-gui/ 2>/dev/null || true
;;
Regular → Executable
+8 -9
View File
@@ -1,9 +1,8 @@
#!/usr/bin/env python3
"""Gera, a partir da logo oficial, os arquivos derivados usados no app e nos ícones.
"""Gera, a partir da logo oficial, o ícone base usado pelo Tauri.
A logo oficial é apps/gui/ui/guara.png (RGBA, fundo transparente, serve aos temas claro e escuro).
Ela não é alterada aqui. Saída:
apps/gui/ui/guara-s.png 128 px, recortada e quadrada (barra lateral e favicon)
A logo oficial é `logo.png` na raiz do repositório (RGBA, fundo transparente,
serve aos temas claro e escuro). Ela não é alterada aqui. Saída:
apps/gui/src-tauri/icons/source.png 1024 px, a arte sobre o fundo escuro com cantos arredondados;
o CI e `npx @tauri-apps/cli icon` geram o resto dos ícones.
@@ -13,7 +12,6 @@ import os
from PIL import Image, ImageDraw
RAIZ = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
UI = os.path.join(RAIZ, "apps/gui/ui")
ICONES = os.path.join(RAIZ, "apps/gui/src-tauri/icons")
FUNDO_ICONE = (10, 16, 28) # o azul-noite do fundo da arte original
@@ -30,9 +28,7 @@ def recortar_quadrado(im, margem=0.04):
def main():
logo = recortar_quadrado(Image.open(os.path.join(UI, "guara.png")).convert("RGBA"))
logo.resize((128, 128), Image.LANCZOS).save(os.path.join(UI, "guara-s.png"), optimize=True)
print("guara-s.png")
logo = recortar_quadrado(Image.open(os.path.join(RAIZ, "logo.png")).convert("RGBA"))
lado = 1024
fundo = Image.new("RGBA", (lado, lado), FUNDO_ICONE + (255,))
@@ -42,7 +38,10 @@ def main():
ImageDraw.Draw(mascara).rounded_rectangle((0, 0, lado - 1, lado - 1), radius=round(lado * 0.2237), fill=255)
icone = Image.new("RGBA", (lado, lado), (0, 0, 0, 0))
icone.paste(fundo, (0, 0), mascara)
icone.save(os.path.join(ICONES, "source.png"), optimize=True)
os.makedirs(ICONES, exist_ok=True)
destino = os.path.join(ICONES, "source.png")
icone.save(destino, optimize=True)
os.chmod(destino, 0o644)
print("icons/source.png")
+19 -15
View File
@@ -11,15 +11,16 @@ Precisa de API (URL do repositório na API do Gitea) e TOKEN no ambiente.
import json
import os
import sys
import urllib.parse
import urllib.request
from datetime import datetime, timezone
# (chaves de plataforma, padrão do nome do pacote). {v} = versão sem o "v".
# Sem macOS: não há runner para compilar essa plataforma.
PLATFORMS = [
(["linux-x86_64", "linux-x86_64-appimage"], "Guara_{v}_amd64.AppImage"),
(["linux-aarch64", "linux-aarch64-appimage"], "Guara_{v}_aarch64.AppImage"),
(["windows-x86_64", "windows-x86_64-nsis"], "Guara_{v}_x64-setup.exe"),
(["linux-x86_64", "linux-x86_64-appimage"], ("Guará_{v}_amd64.AppImage", "Guara_{v}_amd64.AppImage")),
(["linux-aarch64", "linux-aarch64-appimage"], ("Guará_{v}_aarch64.AppImage", "Guara_{v}_aarch64.AppImage")),
(["windows-x86_64", "windows-x86_64-nsis"], ("Guará_{v}_x64-setup.exe", "Guara_{v}_x64-setup.exe")),
]
@@ -29,7 +30,8 @@ def api_get(path):
def api_get_text(url):
req = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['TOKEN']}"})
safe_url = urllib.parse.quote(url, safe=":/?&=%")
req = urllib.request.Request(safe_url, headers={"Authorization": f"token {os.environ['TOKEN']}"})
return urllib.request.urlopen(req).read().decode()
@@ -38,9 +40,9 @@ def build(version, notes, assets, pub_date):
Plataforma sem pacote ou sem assinatura sai da lista; o chamador decide se isso é erro."""
platforms = {}
for keys, pattern in PLATFORMS:
name = pattern.format(v=version)
if name not in assets:
for keys, patterns in PLATFORMS:
name = next((n for p in patterns if (n := p.format(v=version)) in assets), None)
if name is None:
continue
url, sig = assets[name]
for k in keys:
@@ -54,20 +56,22 @@ def main():
rel = api_get(f"/releases/tags/{tag}")
by_name = {a["name"]: a for a in rel["assets"]}
assets = {}
for _, pattern in PLATFORMS:
name = pattern.format(v=version)
for _, patterns in PLATFORMS:
name = next((n for p in patterns if (n := p.format(v=version)) in by_name), None)
if name is None:
continue
sig_name = name + ".sig"
if name in by_name and sig_name in by_name:
if sig_name in by_name:
sig = api_get_text(by_name[sig_name]["browser_download_url"])
assets[name] = (by_name[name]["browser_download_url"], sig)
data = build(version, open(notes_path, encoding="utf-8").read(), assets,
datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
# O CI gera o Linux aarch64 (o runner é arm64); o x86_64 vem de uma máquina x86_64 e
# entra depois (scripts/build-gui.sh linux). Exige o Windows e ao menos um Linux.
# Exige os pacotes assinados de Linux (x86_64 e aarch64) e Windows.
platforms = data["platforms"]
if "windows-x86_64" not in platforms or not any(k.startswith("linux-") for k in platforms):
sys.exit(f"faltam pacotes ou assinaturas (achei {sorted(platforms)}); "
"confira se TAURI_SIGNING_PRIVATE_KEY está nos segredos")
for must in ("linux-x86_64", "linux-aarch64", "windows-x86_64"):
if must not in platforms:
sys.exit(f"faltam pacotes ou assinaturas de {must} (achei {sorted(platforms)}); "
"confira se TAURI_SIGNING_PRIVATE_KEY está nos segredos")
with open(out, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
+2 -1
View File
@@ -16,9 +16,10 @@ id=$(curl -fsS "${auth[@]}" "$API/releases/tags/$tag" | jq -r .id)
for file in "$@"; do
name=$(basename "$file")
encoded=$(jq -rn --arg n "$name" '$n|@uri')
for old in $(curl -fsS "${auth[@]}" "$API/releases/$id/assets" | jq -r --arg n "$name" '.[] | select(.name == $n) | .id'); do
curl -fsS -X DELETE "${auth[@]}" "$API/releases/$id/assets/$old"
done
curl -fsS -X POST "${auth[@]}" -F "attachment=@$file" "$API/releases/$id/assets?name=$name" >/dev/null
curl -fsS -X POST "${auth[@]}" -F "attachment=@$file" "$API/releases/$id/assets?name=$encoded" >/dev/null
echo "enviado: $name"
done
+15 -3
View File
@@ -2,8 +2,11 @@
"""Teste de scripts/make-latest-json.py: python3 scripts/test_make_latest_json.py"""
import importlib.util
import os
import sys
import unittest
sys.dont_write_bytecode = True
spec = importlib.util.spec_from_file_location("m", os.path.join(os.path.dirname(__file__), "make-latest-json.py"))
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
@@ -12,7 +15,7 @@ BASE = "https://git.lucasrguerra.dev.br/lucasrguerra/guara/releases/download/v1.
def assets(version="1.3.2"):
names = [p.format(v=version) for _, p in m.PLATFORMS]
names = [patterns[0].format(v=version) for _, patterns in m.PLATFORMS]
return {n: (BASE + n, f"assinatura-de-{n}\n") for n in names}
@@ -25,11 +28,11 @@ class LatestJson(unittest.TestCase):
for k in ("linux-x86_64", "linux-x86_64-appimage", "linux-aarch64", "windows-x86_64", "windows-x86_64-nsis"):
self.assertIn(k, p)
self.assertTrue(p["windows-x86_64"]["url"].endswith("x64-setup.exe"))
self.assertEqual(p["linux-x86_64"]["signature"], "assinatura-de-Guara_1.3.2_amd64.AppImage")
self.assertEqual(p["linux-x86_64"]["signature"], "assinatura-de-Guará_1.3.2_amd64.AppImage")
def test_missing_package_is_left_out(self):
a = assets()
del a["Guara_1.3.2_x64-setup.exe"]
del a["Guará_1.3.2_x64-setup.exe"]
d = m.build("1.3.2", "", a, "2026-10-30T12:00:00Z")
self.assertNotIn("windows-x86_64", d["platforms"])
self.assertIn("linux-x86_64", d["platforms"])
@@ -38,6 +41,15 @@ class LatestJson(unittest.TestCase):
d = m.build("1.3.2", "", assets("1.2.0"), "2026-10-30T12:00:00Z")
self.assertEqual(d["platforms"], {})
def test_accepts_legacy_unaccented_name(self):
a = assets()
old = "Guara_1.3.2_amd64.AppImage"
_, sig = a.pop("Guará_1.3.2_amd64.AppImage")
a[old] = (BASE + old, sig)
d = m.build("1.3.2", "", a, "2026-10-30T12:00:00Z")
self.assertIn("linux-x86_64", d["platforms"])
self.assertTrue(d["platforms"]["linux-x86_64"]["url"].endswith(old))
if __name__ == "__main__":
unittest.main()
+24 -5
View File
@@ -4,9 +4,9 @@
# scripts/verificar.sh rápido (~1 min): formatação, clippy, tradução, scripts, workflows
# scripts/verificar.sh --completo inclui os testes do workspace e o cargo audit
#
# O CI leva de 5 a 30 minutos por execução e roda em 3 sistemas; quase tudo que o faz falhar
# aparece aqui em segundos. Se isto passa, o CI só deve revelar o que é próprio dele (Windows,
# macOS, imagens baixadas, apt do runner).
# O CI leva de 5 a 30 minutos por execução e roda em Linux; quase tudo que o faz falhar
# aparece aqui em segundos. Se isto passa, o CI só deve revelar o que é próprio dele
# (imagens baixadas, apt do runner).
set -uo pipefail
cd "$(dirname "$0")/.."
completo=0
@@ -24,11 +24,30 @@ passo "clippy (workspace, todos os alvos)" env RUSTFLAGS="-D warnings" cargo cli
passo "cobertura da tradução da interface" node scripts/check-ui-i18n.mjs
passo "teste do latest.json" python3 -B scripts/test_make_latest_json.py
passo "sintaxe dos scripts bash" bash -c 'for f in scripts/*.sh .githooks/*; do bash -n "$f" || exit 1; done'
passo "workflows (YAML válido)" python3 -c "
passo "workflows e templates (YAML válido)" python3 -c "
import glob, yaml
for f in glob.glob('.github/workflows/*.yml'):
for f in glob.glob('.gitea/**/*.yml', recursive=True):
yaml.safe_load(open(f))
"
passo "versões consistentes" python3 -c "
import json, re, sys
from pathlib import Path
def cargo_version(path):
m = re.search(r'^version\s*=\s*\"([^\"]+)\"', Path(path).read_text(), re.M)
if not m:
sys.exit(f'versão não encontrada em {path}')
return m.group(1)
versoes = [
cargo_version('Cargo.toml'),
cargo_version('apps/gui/src-tauri/Cargo.toml'),
json.load(open('apps/gui/src-tauri/tauri.conf.json'))['version'],
]
if len(set(versoes)) != 1:
sys.exit(f'versões divergentes: Cargo.toml={versoes[0]}, src-tauri={versoes[1]}, tauri.conf={versoes[2]}')
print(versoes[0])
"
if [ -f apps/gui/src-tauri/Cargo.toml ] && pkg-config --exists webkit2gtk-4.1 2>/dev/null; then
passo "app desktop (cargo check)" env RUSTFLAGS="" cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
else