commit f405ae2bcf29e3976e737f8fcb49dd8aa48f164c Author: Александр <153805936+Cheviiot@users.noreply.github.com> Date: Sat Jul 25 00:49:32 2026 +1000 feat: create autonomous Nivora package repository Maintain a validated cross-distribution package catalog. Automate upstream updates, isolated builds, diagnostics, and direct publication. Build the official GitHub Desktop sources for Linux with working OAuth. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..988d9b3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +*.png binary +*.svg text eol=lf +*.sh text eol=lf +Staplerfile text eol=lf diff --git a/.github/actions/setup-stplr-spec/action.yml b/.github/actions/setup-stplr-spec/action.yml new file mode 100644 index 0000000..127b393 --- /dev/null +++ b/.github/actions/setup-stplr-spec/action.yml @@ -0,0 +1,42 @@ +name: Подготовить stplr-spec +description: Устанавливает закреплённые Go, системные инструменты и stplr-spec + +inputs: + system-packages: + description: Дополнительные пакеты Ubuntu через пробел + required: false + default: "" + +runs: + using: composite + steps: + - name: Установить Go + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 + with: + go-version: 1.26.5 + cache: false + + - name: Установить системные инструменты + if: ${{ inputs.system-packages != '' }} + shell: bash + env: + SYSTEM_PACKAGES: ${{ inputs.system-packages }} + run: | + sudo apt-get update + read -r -a packages <<<"$SYSTEM_PACKAGES" + sudo apt-get install -y "${packages[@]}" + + - name: Собрать закреплённый stplr-spec + shell: bash + env: + STPLR_UTILS_COMMIT: c6ddbb5e4e5637d97bb7b2587729178d715c6c52 + run: | + git clone --filter=blob:none \ + https://altlinux.space/stapler/stplr-utils.git \ + "$RUNNER_TEMP/stplr-utils" + git -C "$RUNNER_TEMP/stplr-utils" checkout "$STPLR_UTILS_COMMIT" + ( + cd "$RUNNER_TEMP/stplr-utils" + GOBIN="$RUNNER_TEMP/bin" go install ./cmd/stplr-spec + ) + echo "$RUNNER_TEMP/bin" >>"$GITHUB_PATH" diff --git a/.github/assets/nivora.png b/.github/assets/nivora.png new file mode 100644 index 0000000..26e211e Binary files /dev/null and b/.github/assets/nivora.png differ diff --git a/.github/workflows/check-updates.yml b/.github/workflows/check-updates.yml new file mode 100644 index 0000000..3899c49 --- /dev/null +++ b/.github/workflows/check-updates.yml @@ -0,0 +1,201 @@ +name: CI · Автономное обновление пакетов + +on: + schedule: + # 03:00 Asia/Vladivostok (UTC+10) + - cron: "0 17 * * *" + workflow_dispatch: + +permissions: + contents: write + issues: write + +concurrency: + group: nivora-autonomous-updates + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + detect: + name: Найти обновления + runs-on: ubuntu-24.04 + timeout-minutes: 30 + outputs: + packages: ${{ steps.updates.outputs.packages }} + has_updates: ${{ steps.updates.outputs.has_updates }} + github_desktop_update: ${{ steps.updates.outputs.github_desktop_update }} + github_desktop_version: ${{ steps.updates.outputs.github_desktop_version }} + steps: + - name: Получить репозиторий + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Подготовить инструменты + uses: ./.github/actions/setup-stplr-spec + with: + system-packages: jq + + - name: Сформировать план обновления + id: updates + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + packages_output="$(tools/package_updates.sh outdated)" + mapfile -t packages <<<"$packages_output" + if [[ -z "$packages_output" ]]; then + packages=() + fi + if [[ "${#packages[@]}" -eq 0 ]]; then + { + echo 'packages=[]' + echo 'has_updates=false' + echo 'github_desktop_update=false' + echo 'github_desktop_version=' + } >>"$GITHUB_OUTPUT" + exit 0 + fi + + packages_json="$( + printf '%s\n' "${packages[@]}" | + jq -Rsc 'split("\n") | map(select(length > 0))' + )" + github_desktop_update=false + github_desktop_version='' + for package in "${packages[@]}"; do + if [[ "$package" == github-desktop ]]; then + github_desktop_update=true + read -r _ github_desktop_version < <( + tools/package_updates.sh check github-desktop + ) + fi + done + { + echo "packages=${packages_json}" + echo 'has_updates=true' + echo "github_desktop_update=${github_desktop_update}" + echo "github_desktop_version=${github_desktop_version}" + } >>"$GITHUB_OUTPUT" + + github-desktop: + name: Подготовить GitHub Desktop + needs: detect + if: ${{ needs.detect.outputs.github_desktop_update == 'true' }} + uses: ./.github/workflows/github-desktop-linux.yml + with: + version: ${{ needs.detect.outputs.github_desktop_version }} + publish: true + permissions: + contents: write + + update: + name: Изолированно обновить и опубликовать + needs: + - detect + - github-desktop + if: >- + ${{ + always() && + needs.detect.result == 'success' && + needs.detect.outputs.has_updates == 'true' + }} + runs-on: ubuntu-24.04 + timeout-minutes: 360 + steps: + - name: Получить репозиторий + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: main + fetch-depth: 0 + + - name: Подготовить инструменты + uses: ./.github/actions/setup-stplr-spec + with: + system-packages: jq rpm shellcheck + + - name: Изолированно обновить и проверить каждый пакет + id: isolated + env: + GITHUB_TOKEN: ${{ github.token }} + PACKAGES_JSON: ${{ needs.detect.outputs.packages }} + AUTONOMOUS_UPDATE_RESULTS_DIR: ${{ runner.temp }}/update-results + run: | + mapfile -t packages < <(jq -r '.[]' <<<"$PACKAGES_JSON") + tools/autonomous_package_updates.sh "${packages[@]}" + + successful_json="$( + jq -Rsc 'split("\n") | map(select(length > 0))' \ + <"$AUTONOMOUS_UPDATE_RESULTS_DIR/successful-packages" + )" + failed_json="$( + cut -f1 "$AUTONOMOUS_UPDATE_RESULTS_DIR/failed-packages" | + jq -Rsc 'split("\n") | map(select(length > 0))' + )" + echo "successful=${successful_json}" >>"$GITHUB_OUTPUT" + echo "failed=${failed_json}" >>"$GITHUB_OUTPUT" + + - name: Сохранить журналы и diff каждого пакета + if: ${{ always() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: autonomous-update-diagnostics-${{ github.run_id }} + path: ${{ runner.temp }}/update-results + if-no-files-found: error + retention-days: 30 + + - name: Применить только успешные обновления + env: + AUTONOMOUS_UPDATE_RESULTS_DIR: ${{ runner.temp }}/update-results + run: | + while IFS= read -r package; do + [[ -n "$package" ]] || continue + git apply \ + "$AUTONOMOUS_UPDATE_RESULTS_DIR/${package}/update.patch" + done <"$AUTONOMOUS_UPDATE_RESULTS_DIR/successful-packages" + tools/sync_readme_versions.py + tools/run_checks.sh + + - name: Отправить обновления прямо в main + id: publish + env: + SUCCESSFUL_PACKAGES: ${{ steps.isolated.outputs.successful }} + run: | + if git diff --quiet; then + echo 'Изменений после обновления нет' + exit 0 + fi + + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add README.md ./*/Staplerfile + packages="$(jq -r 'join(", ")' <<<"$SUCCESSFUL_PACKAGES")" + git commit -m "chore: autonomously update ${packages}" + git pull --rebase origin main + git push origin HEAD:main + + - name: Синхронизировать постоянные отчёты о сбоях + if: ${{ always() && steps.isolated.outcome == 'success' }} + env: + AUTONOMOUS_UPDATE_RESULTS_DIR: ${{ runner.temp }}/update-results + GH_TOKEN: ${{ github.token }} + run: tools/report_update_failures.sh + + - name: Сообщить о несовместимых пакетах + if: ${{ always() && steps.isolated.outputs.failed != '[]' }} + env: + FAILED_PACKAGES: ${{ steps.isolated.outputs.failed }} + AUTONOMOUS_UPDATE_RESULTS_DIR: ${{ runner.temp }}/update-results + run: | + { + echo '## Требуется ручная диагностика' + echo + echo 'Обновление остальных пакетов продолжено и опубликовано.' + echo 'Для несовместимых пакетов сохранён artifact с логами и diff:' + echo + while IFS=$'\t' read -r package phase; do + printf -- '- **%s**: сбой на фазе **%s**\n' "$package" "$phase" + done <"$AUTONOMOUS_UPDATE_RESULTS_DIR/failed-packages" + } >>"$GITHUB_STEP_SUMMARY" + + echo "::warning::Требуется ручная диагностика: $(jq -r 'join(\", \")' <<<"$FAILED_PACKAGES")" diff --git a/.github/workflows/clean-build.yml b/.github/workflows/clean-build.yml new file mode 100644 index 0000000..fa6ebd1 --- /dev/null +++ b/.github/workflows/clean-build.yml @@ -0,0 +1,47 @@ +name: CI · Ручная сборка пакетов + +on: + workflow_dispatch: + inputs: + packages: + description: "all или package ID через пробел" + required: true + default: all + type: string + +permissions: + contents: read + +concurrency: + group: nivora-clean-build + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + build: + name: Изолированная сборка + runs-on: ubuntu-24.04 + timeout-minutes: 300 + steps: + - name: Получить репозиторий + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Подготовить инструменты + uses: ./.github/actions/setup-stplr-spec + + - name: Собрать пакеты + env: + REQUESTED_PACKAGES: ${{ inputs.packages }} + run: | + if [[ "$REQUESTED_PACKAGES" == all ]]; then + tools/clean_build.sh --all + tools/verify_artifacts.sh --all + NIVORA_DEB_BUILD_MODE=host tools/test_package_lifecycle.sh + else + read -r -a packages <<<"$REQUESTED_PACKAGES" + tools/clean_build.sh "${packages[@]}" + tools/verify_artifacts.sh "${packages[@]}" + fi diff --git a/.github/workflows/github-desktop-linux.yml b/.github/workflows/github-desktop-linux.yml new file mode 100644 index 0000000..670789f --- /dev/null +++ b/.github/workflows/github-desktop-linux.yml @@ -0,0 +1,166 @@ +name: CI · Сборка GitHub Desktop для Linux + +on: + workflow_call: + inputs: + version: + description: "Версия desktop/desktop без префикса release-" + required: true + type: string + publish: + description: "Опубликовать tar.gz в GitHub Release Nivora" + required: false + default: true + type: boolean + workflow_dispatch: + inputs: + version: + description: "Версия desktop/desktop без префикса release-" + required: true + default: "3.6.3" + type: string + publish: + description: "Опубликовать tar.gz в GitHub Release Nivora" + required: true + default: true + type: boolean + +permissions: + contents: write + +concurrency: + group: nivora-github-desktop-linux-${{ inputs.version || '3.6.3' }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + build: + name: Linux ${{ matrix.arch }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + arch: x64 + - runner: ubuntu-24.04-arm + arch: arm64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 180 + steps: + - name: Установить Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: "24.15.0" + + - name: Установить системные зависимости + run: | + sudo apt-get update + sudo apt-get install -y build-essential libsecret-1-dev pkg-config python3 + + - name: Получить официальные исходники GitHub Desktop + env: + VERSION: ${{ inputs.version || '3.6.3' }} + run: | + git clone \ + --branch "release-${VERSION}" \ + --depth 1 \ + --recurse-submodules \ + --shallow-submodules \ + https://github.com/desktop/desktop.git \ + github-desktop + git -C github-desktop remote set-url origin \ + https://github.com/desktop/desktop.git + + - name: Добавить поддержку OAuth callback в Linux + working-directory: github-desktop + run: | + main_process='app/src/main-process/main.ts' + perl -0pi -e \ + "s/if \\(__DEV_SECRETS__\\) \\{\\n possibleProtocols\\.add\\('x-github-desktop-dev-auth'\\)\\n\\} else \\{\\n possibleProtocols\\.add\\('x-github-desktop-auth'\\)\\n\\}/possibleProtocols.add('x-github-desktop-auth')\\nif (__DEV_SECRETS__) {\\n possibleProtocols.add('x-github-desktop-dev-auth')\\n}/" \ + "$main_process" + perl -0pi -e \ + "s/if \\(__WIN32__ && args\\['protocol-launcher'\\] === true\\)/if ((__WIN32__ || __LINUX__) \\&\\& args['protocol-launcher'] === true)/" \ + "$main_process" + grep -Fq "possibleProtocols.add('x-github-desktop-auth')" \ + "$main_process" + grep -Fq "(__WIN32__ || __LINUX__)" "$main_process" + + - name: Установить зависимости из upstream lockfile + working-directory: github-desktop + run: | + node vendor/yarn-1.21.1.js install --frozen-lockfile --non-interactive + + - name: Собрать официальный Electron-клиент + working-directory: github-desktop + env: + NODE_ENV: production + RELEASE_CHANNEL: production + run: node vendor/yarn-1.21.1.js build:prod + + - name: Упаковать Linux-сборку + env: + ARCH: ${{ matrix.arch }} + VERSION: ${{ inputs.version || '3.6.3' }} + run: | + dist="github-desktop/dist/desktop-linux-${ARCH}" + test -x "${dist}/desktop" + grep -aFq 'x-github-desktop-auth' \ + "${dist}/resources/app/main.js" + mkdir -p package/github-desktop + cp -a "${dist}/." package/github-desktop/ + printf '%s\n' "$(git -C github-desktop rev-parse HEAD)" \ + >package/github-desktop/UPSTREAM_COMMIT + tar \ + --sort=name \ + --mtime='UTC 2020-01-01' \ + --owner=0 \ + --group=0 \ + --numeric-owner \ + -C package \ + -cf "github-desktop-${VERSION}-linux-${ARCH}.tar" \ + github-desktop + gzip -n -9 "github-desktop-${VERSION}-linux-${ARCH}.tar" + + - name: Сохранить артефакт + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: github-desktop-${{ inputs.version || '3.6.3' }}-linux-${{ matrix.arch }} + path: github-desktop-${{ inputs.version || '3.6.3' }}-linux-${{ matrix.arch }}.tar.gz + if-no-files-found: error + retention-days: 14 + + publish: + name: Опубликовать Linux-артефакты + if: ${{ inputs.publish }} + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Скачать обе архитектуры + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + pattern: github-desktop-${{ inputs.version || '3.6.3' }}-linux-* + merge-multiple: true + + - name: Создать контрольные суммы + run: sha256sum github-desktop-*.tar.gz >SHA256SUMS + + - name: Создать или обновить GitHub Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + VERSION: ${{ inputs.version || '3.6.3' }} + run: | + tag="github-desktop-${VERSION}-linux" + if ! gh release view "$tag" >/dev/null 2>&1; then + gh release create "$tag" \ + --title "GitHub Desktop ${VERSION} for Linux" \ + --notes "Linux artifacts built by Nivora CI from the official desktop/desktop release-${VERSION} source and its pinned submodules." + fi + gh release upload "$tag" \ + github-desktop-*.tar.gz \ + SHA256SUMS \ + --clobber diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..a40a2e6 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,49 @@ +name: CI · Проверка репозитория + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: nivora-quality-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + quality: + name: Статика, метаданные и тесты + runs-on: ubuntu-24.04 + timeout-minutes: 35 + steps: + - name: Получить репозиторий + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Подготовить инструменты + uses: ./.github/actions/setup-stplr-spec + with: + system-packages: jq rpm shellcheck + + - name: Выполнить проверки + run: tools/run_checks.sh + + - name: Сверить upstream-версии + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set +e + tools/package_updates.sh check-all + status=$? + set -e + if [[ "$status" -eq 10 ]]; then + echo "::warning::Для одного или нескольких пакетов доступны обновления" + elif [[ "$status" -ne 0 ]]; then + exit "$status" + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..094692f --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.deb +*.rpm +*.pkg.tar.* +*.apk +.stplr-spec-bc-export/ +__pycache__/ +*.pyc +.venv/ +.idea/ +.vscode/ +*~ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3c1cc0c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,92 @@ +# История изменений + +В файле фиксируются значимые изменения Nivora. + +## 2026-07-24 — GitHub Desktop + +### Добавлено + +- Добавлен GitHub Desktop 3.6.3 для `amd64` и `arm64`, собираемый напрямую из + официального `desktop/desktop` и официальных submodule-репозиториев GitHub. +- Сторонний Linux-форк `shiftkey/desktop` и готовые артефакты других авторов не + используются. +- Тяжёлая сборка вынесена в отдельный GitHub Actions workflow для нативных + `x64`/`arm64` runner’ов; результаты и SHA-256 публикуются в релизе Nivora. + +## 2026-07-24 — очистка переходной инфраструктуры + +### Изменено + +- Удалены одноразовый мигратор прежнего каталога, переходные package ID, + совместимые команды и относящиеся к ним документация и тесты. +- Транзакционная проверка теперь тестирует обновление внутри Nivora без фиктивных + пакетов стороннего каталога. +- Codex обновлён до `26.721.31836`; новый upstream-сбросил `release` в `1`. +- У Яндекс Браузера canonical desktop-id совпадает с идентификатором окна и XDG + Portal, а прежний ID скрыт и сохранён только для совместимости: GNOME больше + не разделяет launcher и запущенное окно на два приложения. + +## 2026-07-24 — Яндекс Браузер + +### Добавлено + +- В каталог добавлен Яндекс Браузер 26.4.1.1110 из официального stable-репозитория + для `amd64`. +- Сохранены штатные desktop-id, XDG Portal-интеграция, MIME-типы и иконки. +- Обновления браузера переданы Stapler: upstream cron-задача и автозапуск + системной настройки не устанавливаются. + +## 2026-07-24 — Ventoy + +### Добавлено + +- В каталог добавлен Ventoy 1.1.16 с официальным нативным Linux GUI. +- Пакет поддерживает `amd64` и `arm64`, отображается в меню приложений как + **Ventoy** и запрашивает права на работу с накопителями через `pkexec`. + +### Обновлено + +- Clash Verge Rev обновлён до 2.5.2, Claude — до 1.24012.0, + Codex — до 26.721.30844, Happ — до 3.3.6, NetBird — до 0.75.0, + OpenCode — до 1.18.4. +- Tray-иконки Happ переработаны на основе оригинального разорванного H-знака: + графитовая версия для отключённого состояния и лаймовые акценты для соединения. +- Codex и OpenCode возвращены к чистым upstream-бинарникам: удалены runtime/ASAR-патчи, + wrapper OpenCode, лимиты памяти и GPU-fallback; сохранены только системная упаковка, + отображаемые названия и иконки. +- Для Codex восстановлен предусмотренный Linux Computer Use: bundled plugin и + backend `computer-use-linux` 0.4.1 собираются из закреплённых upstream-исходников, + а Linux-патчи взяты из `ilysenko/codex-desktop-linux` без обхода серверного + feature gate. + +## 2026-07-19 — ClaudeAlt и balenaEtcher + +### Добавлено + +- В каталог добавлен balenaEtcher 2.1.6 для безопасной записи образов на + SD-карты и USB-накопители (`amd64`). + +### Изменено + +- Второй профиль Claude заменён самостоятельным приложением ClaudeAlt с отдельными + executable/resources tree, Electron `productName`, Wayland `app_id`, X11 `WM_CLASS`, + desktop-файлом, каталогом данных и Cowork VM socket. +- Старые команды второго аккаунта сохранены как совместимые алиасы ClaudeAlt. +- Существующий каталог `Claude-Account-2` автоматически используется после обновления, + чтобы сохранить авторизацию второго аккаунта. +- ClaudeAlt и Nivora получили новые PNG-иконки с настоящей + прозрачностью вне скруглённых границ. +- Иконка ClaudeAlt подключена к desktop-файлу и Electron `BrowserWindow`. +- Штатный tray-глиф Claude окрашен в оранжевый для основного приложения и в + бирюзовый для ClaudeAlt; большая app-иконка больше не используется в лотке. +- Размер и визуальные отступы иконки ClaudeAlt нормализованы под + upstream-иконку Claude. + +## 2026-07-17 — первый релиз + +### Добавлено + +- Независимый Stapler-каталог из 15 пакетов. +- Новые документация, логотип, валидатор, тесты и GitHub Actions. +- Изолированная сборка всех пакетов и проверка фактического содержимого RPM. +- Транзакционные тесты установки, обновления и удаления DEB/RPM для критичных пакетов. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5828845 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Участие в Nivora + +Проект принимает исправления рецептов, автоматизации и документации. + +## Перед изменением + +1. Проверьте официальное название, package ID, desktop-id и AppStream component-id. +2. Найдите официальный HTTPS-источник для каждой архитектуры. +3. Уточните лицензию и возможность переупаковки. +4. Сверьте зависимости на поддерживаемых дистрибутивах. + +## Требования к пакету + +- Имя каталога точно совпадает с `name`. +- Версия совпадает с upstream; изменение рецепта повышает `release`. +- Скачиваемые sources привязаны к версии и защищены SHA-256. +- `local:///`-файлы присутствуют в каталоге, а скрипты имеют executable bit. +- `provides`, `replaces` и `conflicts` описывают package names, а не имена бинарников или desktop-id. +- Lifecycle-скрипты идемпотентны и не удаляют пользовательские данные. +- `files()` описывает весь payload и не захватывает чужие пути. + +## Проверка + +```bash +tools/run_checks.sh +stplr-spec verify-checksums --path package/Staplerfile +tools/clean_build.sh package +``` + +Для изменения package ID нужны отдельные DEB/RPM-тесты замены и проверка +сохранности данных. + +## Pull request + +Один pull request должен решать одну задачу. В описании укажите: + +- что изменилось; +- ссылку на upstream; +- выполненные проверки и сборки; +- оставшиеся ограничения. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..39bb9f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Cheviiot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f85040c --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +

+ Nivora +

+ +# Nivora + +**Независимый каталог Linux-приложений для Stapler.** + +Nivora предлагает открытые рецепты упаковки десктопных приложений и +инструментов, которых может не быть в системном репозитории. Это не +официальный репозиторий Stapler и не официальные пакеты upstream-проектов. + + +**19 пакетов** · **6 категорий** · `amd64`, `arm64` и `all` + +[![Проверка Nivora](https://github.com/Cheviiot/Nivora/actions/workflows/quality.yml/badge.svg)](https://github.com/Cheviiot/Nivora/actions/workflows/quality.yml) + +## Подключение + +Требуется [Stapler](https://stplr.dev/docs/intro/) `v0.1.1` или новее. + +```bash +sudo stplr repo add nivora https://github.com/Cheviiot/Nivora.git +sudo stplr refresh +``` + +Установка пакета: + +```bash +sudo stplr install nivora/codex +``` + +Проверить описание до установки: + +```bash +stplr info nivora/codex +``` + +## Каталог + +Название ведёт на источник приложения. `all` означает, что сам пакет не +содержит архитектурно-зависимых бинарников. + +### Интернет, сеть и VPN + +| Приложение | Версия | Архитектуры | Установка | +|:--|:--:|:--:|:--| +| [Clash Verge Rev](https://github.com/clash-verge-rev/clash-verge-rev) | `2.5.2` | `amd64`, `arm64` | `stplr install nivora/clash-verge-rev` | +| [Happ](https://happ.su/) | `3.3.6` | `amd64`, `arm64` | `stplr install nivora/happ` | +| [NetBird](https://netbird.io/) | `0.75.0` | `amd64`, `arm64` | `stplr install nivora/netbird` | +| [Tailscale](https://tailscale.com/) | `1.98.9` | `amd64`, `arm64` | `stplr install nivora/tailscale` | +| [Яндекс Браузер](https://browser.yandex.ru/) | `26.4.1.1110` | `amd64` | `stplr install nivora/yandex-browser-stable` | + +### Удалённый доступ + +| Приложение | Версия | Архитектуры | Установка | +|:--|:--:|:--:|:--| +| [Parsec](https://parsec.app/downloads) | `150-104a` | `amd64` | `stplr install nivora/parsec` | + +### AI и разработка + +| Приложение | Версия | Архитектуры | Установка | +|:--|:--:|:--:|:--| +| [Chatbox](https://chatboxai.app/ru) | `1.20.3` | `amd64`, `arm64` | `stplr install nivora/chatbox` | +| [Claude](https://code.claude.com/docs/en/desktop-quickstart) | `1.24012.0` | `amd64`, `arm64` | `stplr install nivora/claude-desktop` | +| [Codex](docs/packages/codex.md) | `26.721.31836` | `amd64` | `stplr install nivora/codex` | +| [GitHub Desktop](docs/packages/github-desktop.md) | `3.6.3` | `amd64`, `arm64` | `stplr install nivora/github-desktop` | +| [OpenCode](https://opencode.ai/) | `1.18.4` | `amd64`, `arm64` | `stplr install nivora/opencode` | + +### Рабочий стол + +| Приложение | Версия | Архитектуры | Установка | +|:--|:--:|:--:|:--| +| [Adwyra](https://github.com/Cheviiot/Adwyra) | `0.6.1` | `all` | `stplr install nivora/adwyra` | +| [AniDesk](https://github.com/theDesConnet/AniDesk) | `0.0.1-beta.7` | `amd64` | `stplr install nivora/anidesk` | + +### Игры + +| Приложение | Версия | Архитектуры | Установка | +|:--|:--:|:--:|:--| +| [PineconeMC](https://pineconemc.com/) | `11.0.3` | `amd64`, `arm64` | `stplr install nivora/pineconemc` | +| [Vual](https://github.com/Cheviiot/Vual) | `0.3.1` | `all` | `stplr install nivora/vual` | + +### Системные инструменты + +| Приложение | Версия | Архитектуры | Установка | +|:--|:--:|:--:|:--| +| [balenaEtcher](https://etcher.balena.io/) | `2.1.6` | `amd64` | `stplr install nivora/balena-etcher` | +| [Fisher](https://github.com/jorgebucaran/fisher) | `4.4.8` | `all` | `stplr install nivora/fisher` | +| [Nivora Stapler Helper](docs/packages/nivora-stplr.md) | `0.3.0` | `all` | `stplr install nivora/nivora-stplr` | +| [Ventoy](docs/packages/ventoy.md) | `1.1.17` | `amd64`, `arm64` | `stplr install nivora/ventoy` | + +## Обновление + +```bash +sudo stplr refresh +sudo stplr upgrade +``` + +Рецепты сохраняют пути пользовательских конфигураций. Обычное обновление и +удаление пакета не должно сбрасывать настройки или выполнять logout. + +## Безопасность и доверие + +- Каждый `Staplerfile` доступен для проверки. +- Файлы загружаются из указанных upstream-источников. +- SHA-256 проверяет целостность загрузки, но не делает upstream автоматически безопасным. +- Условия проприетарных приложений определяются их разработчиками. +- Наличие CI не обещает абсолютную безопасность или совместимость с любой системой. + +Подробнее: [модель доверия](docs/security-model.md) и [политика безопасности](SECURITY.md). + +## Для сопровождающих + +```bash +tools/run_checks.sh +tools/package_updates.sh check-all +tools/clean_build.sh --all +tools/verify_artifacts.sh --all +tools/test_package_lifecycle.sh +``` + +Правила изменений описаны в [CONTRIBUTING.md](CONTRIBUTING.md), а порядок сопровождения — в +[docs/maintenance.md](docs/maintenance.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5e5c53b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Политика безопасности + +## Что относится к Nivora + +Сообщайте о проблемах в: + +- `Staplerfile`, wrappers и lifecycle-скриптах; +- неверных source URL или checksum; +- опасном поведении при установке, обновлении или удалении; +- потере или неожиданной передаче пользовательских данных из-за логики пакета; +- секретах, случайно попавших в репозиторий. + +Уязвимости самого приложения следует передавать upstream-разработчику. + +## Как сообщить + +Для обычной ошибки создайте [issue](https://github.com/Cheviiot/Nivora/issues) и укажите: + +- пакет и архитектуру; +- дистрибутив и версию Stapler; +- точную команду и полный текст ошибки; +- ожидаемое и фактическое поведение. + +Не публикуйте пароли, API-ключи, setup keys, cookies и диагностические архивы без +предварительной проверки. + +Подробная модель доверия: [docs/security-model.md](docs/security-model.md). diff --git a/adwyra/.stapler/update-check b/adwyra/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/adwyra/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/adwyra/LICENSE b/adwyra/LICENSE new file mode 100644 index 0000000..7543550 --- /dev/null +++ b/adwyra/LICENSE @@ -0,0 +1,13 @@ +Сведения о лицензии пакета Adwyra + +Лицензия: GPL-3.0-or-later. +Исходный проект: https://github.com/Cheviiot/Adwyra + +Нюансы пакета: +- Пакет устанавливает исходный Python/GTK-проект Adwyra в системную файловую структуру. +- Код приложения распространяется на условиях GNU GPL версии 3 или любой более поздней версии. +- При распространении измененных версий нужно сохранять условия GPL и предоставлять соответствующий исходный код. +- Пакет не добавляет закрытых бинарных компонентов. + +Полный официальный текст GPL-3.0: +https://www.gnu.org/licenses/gpl-3.0.txt diff --git a/adwyra/Staplerfile b/adwyra/Staplerfile new file mode 100644 index 0000000..29c4c47 --- /dev/null +++ b/adwyra/Staplerfile @@ -0,0 +1,123 @@ +name='adwyra' +version='0.6.1' +release=3 +summary='Minimal GNOME application launcher' +summary_ru='Минималистичный лаунчер приложений для GNOME' +desc='Adwyra is a compact GTK 4/libadwaita application launcher for GNOME with an application grid, search, folders, pinned apps, drag-and-drop ordering, themes, hotkeys and plugin support.' +desc_ru='Adwyra — компактный лаунчер приложений для GNOME на GTK 4/libadwaita с сеткой приложений, поиском, папками, закреплением, drag-and-drop сортировкой, темами, горячими клавишами и плагинами.' +group='Graphical desktop/GNOME' +homepage='https://github.com/Cheviiot/Adwyra' +maintainer='Cheviiot ' + +architectures=('all') + +license=('GPL-3.0-or-later') + +provides=() +replaces=('adwyra') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +# Зависимости для запуска +deps=( + 'python3' + 'gtk4' + 'libadwaita' +) + +# Скрипты установки/удаления +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) +deps_debian=( + 'python3' + 'python3-gi' + 'gir1.2-gtk-4.0' + 'gir1.2-adw-1' +) +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=( + 'python3' + 'python3-gobject' + 'gtk4' + 'libadwaita' +) +deps_arch=( + 'python' + 'python-gobject' + 'gtk4' + 'libadwaita' +) +deps_altlinux=( + 'python3' + 'python3-module-pygobject3' + 'libgtk4' + 'libadwaita' +) +deps_opensuse=( + 'python3' + 'python3-gobject' + 'gtk4' + 'libadwaita' +) + +# Источники - используем архив для предотвращения проблем с кэшем +sources=( + "https://github.com/Cheviiot/adwyra/archive/refs/tags/v${version}.tar.gz" + 'local:///LICENSE' +) +checksums=( + '4acb67460a6d302d5c60c369fe68e616baff2aeafb760c421cb65a54dc8df39a' + '4acd6128d4fbff11fd374e862520cf93aacad91650de1fe16fc4e424bd1ddc14' +) + +# AppStream +appstream_app_id='com.github.adwyra' + +package() { + # GitHub создаёт архив с именем репозитория (Adwyra) + src="${srcdir}/Adwyra-${version}" + + # Бинарный файл + install -Dm755 "${src}/adwyra" "${pkgdir}/usr/bin/adwyra" + + # Python модуль + install -dm755 "${pkgdir}/usr/lib/adwyra" + cp -r "${src}/src/adwyra" "${pkgdir}/usr/lib/adwyra/" + find "${pkgdir}/usr/lib/adwyra" -depth -path '*/__pycache__*' -delete + + # Обновляем путь в бинарнике + sed -i 's|os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")|'\''/usr/lib/adwyra'\''|' \ + "${pkgdir}/usr/bin/adwyra" + + # Desktop файл + install-desktop "${src}/data/com.github.adwyra.desktop" + + # Иконки всех размеров + for size in 16 24 32 48 64 128 256 512; do + install -Dm644 "${src}/data/icons/hicolor/${size}x${size}/apps/com.github.adwyra.png" \ + "${pkgdir}/usr/share/icons/hicolor/${size}x${size}/apps/com.github.adwyra.png" + done + + # AppStream metainfo + install -Dm644 "${src}/data/com.github.adwyra.metainfo.xml" \ + "${pkgdir}/usr/share/metainfo/com.github.adwyra.metainfo.xml" + + # Лицензия + install-license "${src}/LICENSE" "adwyra/LICENSE.upstream" + install-license "${srcdir}/LICENSE" "adwyra/LICENSE.nivora" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/usr/lib/adwyra/**/*" \ + "/usr/share/icons/hicolor/*/apps/com.github.adwyra.png" \ + "/usr/share/metainfo/com.github.adwyra.metainfo.xml" +} diff --git a/adwyra/postinstall.sh b/adwyra/postinstall.sh new file mode 100755 index 0000000..3dab81c --- /dev/null +++ b/adwyra/postinstall.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +# Обновляем кэш иконок +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +# Обновляем desktop базу данных +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi diff --git a/adwyra/postremove.sh b/adwyra/postremove.sh new file mode 100755 index 0000000..3dab81c --- /dev/null +++ b/adwyra/postremove.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +# Обновляем кэш иконок +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +# Обновляем desktop базу данных +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi diff --git a/adwyra/stapler-repo.toml b/adwyra/stapler-repo.toml new file mode 100644 index 0000000..d35f9bf --- /dev/null +++ b/adwyra/stapler-repo.toml @@ -0,0 +1 @@ +[inherit] diff --git a/anidesk/.stapler/update-check b/anidesk/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/anidesk/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/anidesk/.stapler/update-run b/anidesk/.stapler/update-run new file mode 100755 index 0000000..af9ff2a --- /dev/null +++ b/anidesk/.stapler/update-run @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +new_version="${1:?new version is required}" +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +staplerfile="${package_dir}/Staplerfile" +debian_version="${new_version/-beta./.beta.}" + +stplr-spec set-field --path "$staplerfile" version "$new_version" +stplr-spec set-field --path "$staplerfile" release 1 +stplr-spec set-field --path "$staplerfile" debian_version "$debian_version" +stplr-spec update-checksums --path "$staplerfile" diff --git a/anidesk/LICENSE b/anidesk/LICENSE new file mode 100644 index 0000000..1aa15d8 --- /dev/null +++ b/anidesk/LICENSE @@ -0,0 +1,21 @@ +Сведения о лицензии пакета AniDesk + +Лицензия: GPL-2.0-only. +Исходный проект: https://github.com/theDesConnet/AniDesk +Официальная страница релизов: https://github.com/theDesConnet/AniDesk/releases +Сайт проекта: https://anidesk.ds1nc.ru + +Нюансы пакета: +- Пакет перепаковывает официальный Linux deb-артефакт AniDesk из релиза theDesConnet/AniDesk. +- AniDesk является неофициальным desktop-клиентом Anixart; сервис Anixart, учетные записи, API и сетевые функции не входят в пакет и регулируются правилами соответствующих сервисов. +- Встроенный видеоплеер использует внешние источники и сетевые запросы приложения. Доступность отдельных источников зависит от upstream-приложения и внешних сервисов. +- Anime4K-апскейл использует WebGPU/Vulkan. Для него нужны рабочие драйверы GPU, Vulkan loader и Mesa/проприетарный Vulkan-драйвер; без них базовый запуск приложения возможен, но ускорение видео может быть недоступно. +- Команда `anidesk` запускает upstream-приложение без отключения GPU. Если из-за драйвера Vulkan/GLX приложение падает на старте, можно временно запустить `ANIDESK_DISABLE_GPU=1 anidesk`; этот режим отключает GPU-ускорение Chromium и не предназначен для Anime4K/WebGPU. +- Upstream deb поставляет иконку `anidesk.png` как ICO-контейнер с неверным расширением. Пакет Nivora заменяет ее на PNG-экспорт того же изображения, чтобы desktop-меню корректно показывали иконку. +- Upstream-приложение создает API-клиент только при старте renderer-страницы. Пакет Nivora добавляет runtime-патч: после смены endpoint API в настройках renderer автоматически перезагружается, чтобы контент сразу грузился с выбранного адреса. +- Discord Rich Presence работает только при установленном и запущенном Discord-клиенте с доступным локальным IPC. +- Пользовательские настройки, авторизация, кэш, история и загруженные данные создаются приложением в домашнем каталоге пользователя после запуска и не поставляются внутри пакета. +- В `/usr/share/licenses/anidesk` дополнительно устанавливаются bundled-лицензии Electron/Chromium, которые поставляет upstream-артефакт. + +Полный официальный текст GPL-2.0: +https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt diff --git a/anidesk/Staplerfile b/anidesk/Staplerfile new file mode 100644 index 0000000..79e7ac1 --- /dev/null +++ b/anidesk/Staplerfile @@ -0,0 +1,236 @@ +name='anidesk' +version='0.0.1-beta.7' +debian_version='0.0.1.beta.7' +release=5 +summary='Unofficial Anixart desktop client' +summary_ru='Неофициальный desktop-клиент Anixart' +group='Video/Players' +desc='AniDesk is an unofficial Electron desktop client for Anixart with account support, a built-in video player and Anime4K WebGPU upscaling.' +desc_ru='AniDesk — неофициальный Electron desktop-клиент Anixart с поддержкой аккаунта, встроенным видеоплеером и Anime4K-апскейлом через WebGPU.' +homepage='https://github.com/theDesConnet/AniDesk' +maintainer='DesConnet, hack1exe ' + +architectures=('amd64') + +license=('GPL-2.0-only') + +provides=() +replaces=('anidesk') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('libgtk-3-0' 'libnotify4' 'libnss3' 'xdg-utils' 'libatspi2.0-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2' 'libvulkan1' 'mesa-vulkan-drivers') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'mesa-libgbm' 'libxcb' 'alsa-lib' 'vulkan-loader' 'mesa-vulkan-drivers') +deps_arch=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'mesa' 'libxcb' 'alsa-lib' 'vulkan-icd-loader' 'vulkan-radeon') +deps_opensuse=('gtk3' 'libnotify4' 'mozilla-nss' 'xdg-utils' 'at-spi2-core' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2' 'libvulkan1' 'Mesa-vulkan-device-select') +deps_altlinux=('libgtk+3' 'libnotify' 'libnss' 'xdg-utils' 'libat-spi2-core' 'libdrm' 'libgbm' 'libxcb' 'libalsa' 'libvulkan1' 'mesa-dri-drivers') + +build_deps=('binutils') +build_deps_debian=('binutils' 'python3') +build_deps_ubuntu=("${build_deps_debian[@]}") +build_deps_fedora=('binutils' 'python3') +build_deps_arch=('binutils' 'python') +build_deps_opensuse=('binutils' 'python3') +build_deps_altlinux=('binutils' 'python3') + +opt_deps=() +opt_deps_debian=('gnome-keyring' 'discord') +opt_deps_ubuntu=("${opt_deps_debian[@]}") +opt_deps_fedora=('gnome-keyring' 'discord') +opt_deps_arch=('gnome-keyring' 'discord') +opt_deps_opensuse=('gnome-keyring' 'discord') +opt_deps_altlinux=('gnome-keyring') + +sources=( + "https://github.com/theDesConnet/AniDesk/releases/download/v${version}/anidesk_${debian_version}_amd64.deb?~archive=false&~name=anidesk.deb" + "local:///anidesk.png" + "local:///anidesk-wrapper" + "local:///LICENSE" +) + +checksums=( + 'sha256:70fc1207824757f7ddab63f9e210f31bb907ea43dee3d2827fe79c3ef00252b6' + 'sha256:6d7791424bd7db712b121f7e66206289b1d31eb2a8ea998d9aeced2c7075dfe8' + 'sha256:f58b9f720bd63ff49053f949dd3f0a738afc2aa2cee57fedbd2b73440c123721' + 'sha256:892e6b268f44a850a102cc6367fec45853fe3556840d44a6106f08487553e6c4' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x anidesk.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + install -dm755 "${pkgdir}/usr/lib" + cp -a "${srcdir}/extracted/usr/lib/anidesk" "${pkgdir}/usr/lib/" + chmod 0755 "${pkgdir}/usr/lib/anidesk/chrome-sandbox" + chmod 755 "${pkgdir}/usr/lib/anidesk/anidesk" + + python3 - "${pkgdir}/usr/lib/anidesk/resources/app.asar" <<'PY' +import copy +import hashlib +import json +import struct +import sys +from pathlib import Path + +asar_path = Path(sys.argv[1]) +data = asar_path.read_bytes() +_, _, _, header_size = struct.unpack("{x.set(t),set(o,!0),' + 'baseSettings.restartRequired=!0}' + ) + new = ( + 'onChangeCallback:(e,t)=>{get(n)!==t&&(x.set(t),set(o,!0),' + 'baseSettings.restartRequired=!0,queueMicrotask(()=>location.reload()))}' + ) + if old not in text: + raise SystemExit("AniDesk ASAR patch failed: endpoint dropdown marker not found") + patched[path_name] = text.replace(old, new, 1).encode() + bundle_patch_applied = True + elif path_name == "src/app/settings-pages/SettingsApp.svelte": + text = content.decode() + old = """ onChangeCallback={(e, v) => { + endpointUrlRaw.set(v); + restartRequired = true; + baseSettings.restartRequired = true; + }} +""" + new = """ onChangeCallback={(e, v) => { + if (endpointUrl === v) { + return; + } + endpointUrlRaw.set(v); + restartRequired = true; + baseSettings.restartRequired = true; + queueMicrotask(() => window.location.reload()); + }} +""" + if old not in text: + raise SystemExit("AniDesk ASAR patch failed: endpoint source marker not found") + patched[path_name] = text.replace(old, new, 1).encode() + source_patch_applied = True + +if not bundle_patch_applied: + raise SystemExit("AniDesk ASAR patch failed: runtime bundle was not patched") +if not source_patch_applied: + raise SystemExit("AniDesk ASAR patch failed: source view was not patched") + + +def rebuild(node, prefix="", offset_state=None, payload=None): + if offset_state is None: + offset_state = {"offset": 0} + if payload is None: + payload = [] + new_node = copy.deepcopy(node) + for name, entry in list(new_node.get("files", {}).items()): + path_name = f"{prefix}{name}" + if "files" in entry: + new_node["files"][name] = rebuild( + entry, f"{path_name}/", offset_state, payload + ) + continue + content = None + for candidate_path, candidate_content in entries: + if candidate_path == path_name: + content = patched.get(path_name, candidate_content) + break + if content is None: + continue + entry["size"] = len(content) + entry["offset"] = str(offset_state["offset"]) + digest = hashlib.sha256(content).hexdigest() + entry["integrity"] = { + "algorithm": "SHA256", + "hash": digest, + "blockSize": 4194304, + "blocks": [digest], + } + offset_state["offset"] += len(content) + payload.append(content) + if prefix == "": + return new_node, payload + return new_node + + +new_header, payload = rebuild(header) +header_json = json.dumps(new_header, separators=(",", ":")).encode() +header_padded_size = (len(header_json) + 3) & ~3 +asar_path.write_bytes( + struct.pack("&2 + fi +} + +sandbox=/usr/lib/anidesk/chrome-sandbox +if [[ ! -f "$sandbox" ]]; then + echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2 + exit 1 +fi +if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \ + && unshare --user true 2>/dev/null; then + chmod 0755 "$sandbox" +else + chmod 4755 "$sandbox" +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/anidesk/postremove.sh b/anidesk/postremove.sh new file mode 100755 index 0000000..44d2528 --- /dev/null +++ b/anidesk/postremove.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/anidesk/stapler-repo.toml b/anidesk/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/anidesk/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/balena-etcher/.stapler/update-check b/balena-etcher/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/balena-etcher/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/balena-etcher/Staplerfile b/balena-etcher/Staplerfile new file mode 100644 index 0000000..42538b7 --- /dev/null +++ b/balena-etcher/Staplerfile @@ -0,0 +1,144 @@ +name='balena-etcher' +version='2.1.6' +release=1 +summary='Safe SD card and USB drive image flasher' +summary_ru='Безопасная запись образов на SD-карты и USB-накопители' +group='System/Configuration/Hardware' +desc='balenaEtcher is a graphical utility for writing operating-system images to SD cards and USB drives with validation after flashing.' +desc_ru='balenaEtcher — графическая утилита для записи образов операционных систем на SD-карты и USB-накопители с проверкой данных после прошивки.' +homepage='https://etcher.balena.io/' +maintainer='Balena Ltd. ' + +architectures=('amd64') + +license=('Apache-2.0') + +provides=() +replaces=('balena-etcher') +conflicts=() + +# Electron и нативный helper поставляются в официальном DEB. +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=( + 'libgtk-3-0' 'libnotify4' 'libnss3' 'libxtst6' 'xdg-utils' + 'libatspi2.0-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2' + 'libatk1.0-0' 'libc6' 'libcairo2' 'libcups2' 'libdbus-1-3' 'libexpat1' + 'libfontconfig1' 'libfreetype6' 'libgcc1' 'libgdk-pixbuf2.0-0 | libgdk-pixbuf-2.0-0' + 'libglib2.0-0' 'liblzma5' 'libnspr4' 'libpango1.0-0 | libpango-1.0-0' + 'libstdc++6' 'libx11-6' 'libxcomposite1' 'libxcursor1' 'libxdamage1' + 'libxext6' 'libxfixes3' 'libxi6' 'libxrandr2' 'libxrender1' 'libxss1' + 'kde-cli-tools | kde-runtime | trash-cli | libglib2.0-bin | gvfs-bin' + 'polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1' +) +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=( + 'gtk3' 'libnotify' 'nss' 'libXtst' 'xdg-utils' 'at-spi2-core' + 'libdrm' 'mesa-libgbm' 'libxcb' 'alsa-lib' 'atk' 'glibc' 'cairo' + 'cups-libs' 'dbus-libs' 'expat' 'fontconfig' 'freetype' 'libgcc' + 'gdk-pixbuf2' 'glib2' 'xz-libs' 'nspr' 'pango' 'libstdc++' 'libX11' + 'libXcomposite' 'libXcursor' 'libXdamage' 'libXext' 'libXfixes' 'libXi' + 'libXrandr' 'libXrender' 'libXScrnSaver' 'polkit' +) +deps_arch=( + 'gtk3' 'libnotify' 'nss' 'libxtst' 'xdg-utils' 'at-spi2-core' + 'libdrm' 'mesa' 'libxcb' 'alsa-lib' 'atk' 'glibc' 'cairo' 'cups' + 'dbus' 'expat' 'fontconfig' 'freetype2' 'gcc-libs' 'gdk-pixbuf2' + 'glib2' 'xz' 'nspr' 'pango' 'libx11' 'libxcomposite' 'libxcursor' + 'libxdamage' 'libxext' 'libxfixes' 'libxi' 'libxrandr' 'libxrender' + 'libxss' 'polkit' +) +deps_opensuse=( + 'gtk3' 'libnotify4' 'mozilla-nss' 'libXtst6' 'xdg-utils' 'at-spi2-core' + 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2' 'libatk-1_0-0' + 'glibc' 'libcairo2' 'libcups2' 'libdbus-1-3' 'libexpat1' + 'fontconfig' 'libfreetype6' 'libgcc_s1' 'gdk-pixbuf-loader-rsvg' + 'glib2' 'liblzma5' 'mozilla-nspr' 'libpango-1_0-0' 'libstdc++6' + 'libX11-6' 'libXcomposite1' 'libXcursor1' 'libXdamage1' 'libXext6' + 'libXfixes3' 'libXi6' 'libXrandr2' 'libXrender1' 'libXss1' 'polkit' +) +deps_altlinux=( + 'libgtk+3' 'libnotify' 'libnss' 'libXtst' 'xdg-utils' 'libat-spi2-core' + 'libdrm' 'libgbm' 'libxcb' 'libalsa' 'libatk' 'glibc' 'libcairo' + 'libcups' 'libdbus' 'libexpat' 'fontconfig' 'libfreetype' 'libgcc1' + 'libgdk-pixbuf' 'glib2' 'liblzma' 'libnspr' 'libpango' 'libstdc++6' + 'libX11' 'libXcomposite' 'libXcursor' 'libXdamage' 'libXext' 'libXfixes' + 'libXi' 'libXrandr' 'libXrender' 'libXScrnSaver' 'polkit' +) + +opt_deps=() +opt_deps_debian=() +opt_deps_ubuntu=("${opt_deps_debian[@]}") +opt_deps_fedora=('polkit-gnome | polkit-kde' 'gvfs') +opt_deps_arch=('polkit-gnome | polkit-kde-agent' 'gvfs') +opt_deps_opensuse=('polkit-gnome | polkit-kde-agent-6' 'glib2-tools') +opt_deps_altlinux=( + 'polkit-gnome | polkit-kde-agent | lxqt-policykit | xfce-polkit' + 'gvfs' +) + +build_deps=('binutils') + +sources=( + "https://github.com/balena-io/etcher/releases/download/v${version}/balena-etcher_${version}_amd64.deb?~archive=false&~name=balena-etcher.deb" + "https://raw.githubusercontent.com/balena-io/etcher/v${version}/LICENSE?~archive=false&~name=LICENSE" +) + +checksums=( + 'sha256:2bdebb46c9f750a9abf11c188ff69a405b4a4fed114333d634c3b3fe59a64057' + 'sha256:39445b459f86621683f9731fb6a7d070819dc379840e8ef52f62bb1c68942291' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x balena-etcher.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + install -dm755 "${pkgdir}/usr/lib" + cp -a "${srcdir}/extracted/usr/lib/balena-etcher" "${pkgdir}/usr/lib/" + chmod 0755 "${pkgdir}/usr/lib/balena-etcher/chrome-sandbox" + chmod 0755 \ + "${pkgdir}/usr/lib/balena-etcher/balena-etcher" \ + "${pkgdir}/usr/lib/balena-etcher/resources/etcher-util" + + # Upstream DEB содержит balenaEtcher-ссылку на каталог GitHub Actions. + # Сохраняем совместимое имя, но направляем его на локальный executable. + ln -sfn balena-etcher "${pkgdir}/usr/lib/balena-etcher/balenaEtcher" + install -dm755 "${pkgdir}/usr/bin" + ln -sfn ../lib/balena-etcher/balena-etcher \ + "${pkgdir}/usr/bin/balena-etcher" + + install -Dm644 \ + "${srcdir}/extracted/usr/share/applications/balena-etcher.desktop" \ + "${pkgdir}/usr/share/applications/balena-etcher.desktop" + install -Dm644 \ + "${srcdir}/extracted/usr/share/pixmaps/balena-etcher.png" \ + "${pkgdir}/usr/share/pixmaps/balena-etcher.png" + + install-license LICENSE "balena-etcher/LICENSE" + install-license \ + "${srcdir}/extracted/usr/share/doc/balena-etcher/copyright" \ + "balena-etcher/copyright" + install-license \ + "${srcdir}/extracted/usr/lib/balena-etcher/LICENSES.chromium.html" \ + "balena-etcher/LICENSES.chromium.html" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/usr/lib/balena-etcher/**/*" \ + "/usr/share/pixmaps/balena-etcher.png" +} diff --git a/balena-etcher/postinstall.sh b/balena-etcher/postinstall.sh new file mode 100755 index 0000000..b36c9aa --- /dev/null +++ b/balena-etcher/postinstall.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +sandbox=/usr/lib/balena-etcher/chrome-sandbox +if [[ ! -f "$sandbox" ]]; then + echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2 + exit 1 +fi +if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \ + && unshare --user true 2>/dev/null; then + chmod 0755 "$sandbox" +else + chmod 4755 "$sandbox" +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/balena-etcher/postremove.sh b/balena-etcher/postremove.sh new file mode 100755 index 0000000..684bfe1 --- /dev/null +++ b/balena-etcher/postremove.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/balena-etcher/stapler-repo.toml b/balena-etcher/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/balena-etcher/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/chatbox/.stapler/update-check b/chatbox/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/chatbox/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/chatbox/LICENSE b/chatbox/LICENSE new file mode 100644 index 0000000..dd97e7f --- /dev/null +++ b/chatbox/LICENSE @@ -0,0 +1,20 @@ +Сведения о лицензии пакета Chatbox + +Лицензия: GPL-3.0-only. +Исходный проект: https://github.com/chatboxai/chatbox +Официальный сайт: https://chatboxai.app/ru +Официальная страница загрузки: https://chatboxai.app/ru +Условия использования сервиса: https://chatboxai.app/ru/terms +Политика конфиденциальности: https://chatboxai.app/ru/privacy + +Нюансы пакета: +- Пакет перепаковывает официальный Linux deb-артефакт Chatbox Desktop из релиза `chatboxai/chatbox`. +- Chatbox Community Edition распространяется как открытый клиент под GPL-3.0; bundled-зависимости Electron/Chromium устанавливаются вместе с upstream-артефактом. +- Последние релизы Chatbox могут выходить без Linux-артефактов. Пакет Nivora использует последний upstream-релиз, где реально опубликованы Linux deb-файлы для amd64 и arm64. +- Chatbox может работать с Chatbox AI Service, OpenAI, Claude, Gemini, Ollama и другими внешними провайдерами. Учетные записи, API-ключи, подписки, сетевые функции и модели регулируются условиями соответствующих сервисов. +- История чатов, настройки, кэш, авторизация и локальные базы данных создаются приложением в домашнем каталоге пользователя после запуска и не поставляются внутри пакета. +- Пакет добавляет команды `chatbox`, `chatbox-ai` и `chatbox-desktop` для совместимого запуска приложения. +- В `/usr/share/licenses/chatbox` дополнительно устанавливаются bundled-лицензии Electron/Chromium, если они присутствуют в upstream-артефакте. + +Полный официальный текст GPL-3.0: +https://www.gnu.org/licenses/gpl-3.0.txt diff --git a/chatbox/Staplerfile b/chatbox/Staplerfile new file mode 100644 index 0000000..eedaf7a --- /dev/null +++ b/chatbox/Staplerfile @@ -0,0 +1,115 @@ +name='chatbox' +version='1.20.3' +release=3 +summary='Desktop AI client for multiple LLM providers' +summary_ru='Десктопный AI-клиент для разных LLM-провайдеров' +group='Development/Tools' +desc='Chatbox is a desktop AI client for Chatbox AI, OpenAI, Claude, Gemini, Ollama and other LLM providers with local chat history and file support.' +desc_ru='Chatbox — десктопный AI-клиент для Chatbox AI, OpenAI, Claude, Gemini, Ollama и других LLM-провайдеров с локальной историей чатов и поддержкой файлов.' +homepage='https://chatboxai.app/ru' +maintainer='Chatbox ' + +architectures=('amd64' 'arm64') + +license=('GPL-3.0-only') + +provides=() +replaces=('chatbox') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('libgtk-3-0' 'libnotify4' 'libnss3' 'libxss1' 'libxtst6' 'xdg-utils' 'libatspi2.0-0' 'libuuid1' 'libsecret-1-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('gtk3' 'libnotify' 'nss' 'libXScrnSaver' 'libXtst' 'xdg-utils' 'at-spi2-core' 'libuuid' 'libsecret' 'libdrm' 'mesa-libgbm' 'libxcb' 'alsa-lib') +deps_arch=('gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'libutil-linux' 'libsecret' 'libdrm' 'mesa' 'libxcb' 'alsa-lib') +deps_opensuse=('gtk3' 'libnotify4' 'mozilla-nss' 'libXScrnSaver1' 'libXtst6' 'xdg-utils' 'at-spi2-core' 'libuuid1' 'libsecret-1-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2') +deps_altlinux=('libgtk+3' 'libnotify' 'libnss' 'libXScrnSaver' 'libXtst' 'xdg-utils' 'libat-spi2-core' 'libuuid' 'libsecret' 'libdrm' 'libgbm' 'libxcb' 'libalsa') + +opt_deps=() +opt_deps_debian=('libappindicator3-1' 'gnome-keyring') +opt_deps_ubuntu=("${opt_deps_debian[@]}") +opt_deps_fedora=('libappindicator-gtk3' 'gnome-keyring') +opt_deps_arch=('libappindicator-gtk3' 'gnome-keyring') +opt_deps_opensuse=('libappindicator3-1' 'gnome-keyring') +opt_deps_altlinux=('libappindicator-gtk3' 'gnome-keyring') + +build_deps=('binutils') + +sources=( + "https://github.com/chatboxai/chatbox/releases/download/v${version}/Chatbox-${version}-amd64.deb?~archive=false&~name=chatbox.deb" + 'local:///LICENSE' +) + +sources_arm64=( + "https://github.com/chatboxai/chatbox/releases/download/v${version}/Chatbox-${version}-arm64.deb?~archive=false&~name=chatbox.deb" + 'local:///LICENSE' +) + +checksums=( + 'sha256:7a5b86200c180142ee7d4ed5d56edaabba3a3b37b3b8d8c4eed32678d327e141' + 'sha256:9408c95380fe46177c49bb9ba845dc0d706e88cdd2d637b5d2b9168730713acd' +) + +checksums_arm64=( + 'sha256:909c8ccbb5965a63967af2428210456eb2c9fb7baf88a1a6971e3b07ead81d60' + 'sha256:9408c95380fe46177c49bb9ba845dc0d706e88cdd2d637b5d2b9168730713acd' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x chatbox.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + install -dm755 "${pkgdir}/opt" + cp -a "${srcdir}/extracted/opt/Chatbox" "${pkgdir}/opt/" + chmod 0755 "${pkgdir}/opt/Chatbox/chrome-sandbox" + chmod 755 "${pkgdir}/opt/Chatbox/xyz.chatboxapp.app" + + install -dm755 "${pkgdir}/usr/share" + cp -a "${srcdir}/extracted/usr/share/applications" "${pkgdir}/usr/share/" + cp -a "${srcdir}/extracted/usr/share/icons" "${pkgdir}/usr/share/" + + if [[ -d "${srcdir}/extracted/usr/share/doc" ]]; then + cp -a "${srcdir}/extracted/usr/share/doc" "${pkgdir}/usr/share/" + fi + + sed -i 's/^Categories=.*/Categories=Development;/' \ + "${pkgdir}/usr/share/applications/xyz.chatboxapp.app.desktop" + + install -dm755 "${pkgdir}/usr/bin" + ln -sfn /opt/Chatbox/xyz.chatboxapp.app "${pkgdir}/usr/bin/chatbox" + ln -sfn /opt/Chatbox/xyz.chatboxapp.app "${pkgdir}/usr/bin/chatbox-ai" + ln -sfn /opt/Chatbox/xyz.chatboxapp.app "${pkgdir}/usr/bin/chatbox-desktop" + + install-license LICENSE "chatbox/LICENSE.nivora" + + if [[ -f "${srcdir}/extracted/opt/Chatbox/LICENSE.electron.txt" ]]; then + install-license "${srcdir}/extracted/opt/Chatbox/LICENSE.electron.txt" \ + "chatbox/LICENSE.electron.txt" + fi + if [[ -f "${srcdir}/extracted/opt/Chatbox/LICENSES.chromium.html" ]]; then + install-license "${srcdir}/extracted/opt/Chatbox/LICENSES.chromium.html" \ + "chatbox/LICENSES.chromium.html" + fi +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/opt/Chatbox/**/*" \ + "/usr/share/doc/xyz.chatboxapp.app/*" \ + "/usr/share/icons/hicolor/*/apps/xyz.chatboxapp.app.png" +} diff --git a/chatbox/postinstall.sh b/chatbox/postinstall.sh new file mode 100755 index 0000000..e0793ed --- /dev/null +++ b/chatbox/postinstall.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +sandbox=/opt/Chatbox/chrome-sandbox +if [[ ! -f "$sandbox" ]]; then + echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2 + exit 1 +fi +if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \ + && unshare --user true 2>/dev/null; then + chmod 0755 "$sandbox" +else + chmod 4755 "$sandbox" +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/chatbox/postremove.sh b/chatbox/postremove.sh new file mode 100755 index 0000000..44d2528 --- /dev/null +++ b/chatbox/postremove.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/chatbox/stapler-repo.toml b/chatbox/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/chatbox/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/clash-verge-rev/.stapler/update-check b/clash-verge-rev/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/clash-verge-rev/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/clash-verge-rev/LICENSE b/clash-verge-rev/LICENSE new file mode 100644 index 0000000..fd66f7d --- /dev/null +++ b/clash-verge-rev/LICENSE @@ -0,0 +1,14 @@ +Сведения о лицензии пакета Clash Verge Rev + +Лицензия: GPL-3.0-only. +Исходный проект: https://github.com/clash-verge-rev/clash-verge-rev +Официальный сайт: https://www.clashverge.dev + +Нюансы пакета: +- Пакет перепаковывает официальный deb-артефакт Clash Verge Rev для Linux. +- Приложение распространяется на условиях GNU GPL версии 3. +- GUI-клиент работает с профилями Mihomo/Clash; сами профили, подписки и внешние серверы в пакет не входят. +- При дальнейшем распространении измененных сборок нужно соблюдать требования GPL-3.0-only. + +Полный официальный текст GPL-3.0: +https://www.gnu.org/licenses/gpl-3.0.txt diff --git a/clash-verge-rev/Staplerfile b/clash-verge-rev/Staplerfile new file mode 100644 index 0000000..1a3912f --- /dev/null +++ b/clash-verge-rev/Staplerfile @@ -0,0 +1,91 @@ +name='clash-verge-rev' +version=2.5.2 +release=2 +summary='Tauri GUI client for Mihomo/Clash proxy profiles' +summary_ru='GUI-клиент на Tauri для профилей Mihomo/Clash' +group='Networking/Other' +desc='Clash Verge Rev is a modern cross-platform GUI client based on Tauri for a tailored proxy experience with Mihomo/Clash profiles.' +desc_ru='Clash Verge Rev — современный кроссплатформенный GUI-клиент на Tauri для удобной работы с профилями прокси Mihomo/Clash.' +homepage='https://www.clashverge.dev' +maintainer='clash-verge-rev contributors ' + +architectures=('amd64' 'arm64') + +license=('GPL-3.0-only') + +provides=() +replaces=('clash-verge-rev') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('libc6' 'libgtk-3-0' 'libwebkit2gtk-4.1-0' 'libayatana-appindicator3-1' 'libssl3 | libssl3t64' 'libcap2-bin') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('glibc' 'gtk3' 'webkit2gtk4.1' 'libayatana-appindicator' 'openssl-libs' 'libcap') +deps_arch=('glibc' 'gtk3' 'webkit2gtk-4.1' 'libayatana-appindicator' 'openssl' 'libcap') +deps_opensuse=('glibc' 'gtk3' 'libwebkit2gtk-4_1-0' 'libayatana-appindicator3-1' 'libopenssl3' 'libcap-progs') +deps_altlinux=('glibc' 'libgtk+3' 'libwebkit2gtk4.1' 'libayatana-appindicator3-1' 'libssl3' 'libcap-utils') + +build_deps=('binutils') + +sources=( + "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}/Clash.Verge_${version}_amd64.deb?~archive=false&~name=clash-verge.deb" + 'local:///LICENSE' +) + +sources_arm64=( + "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}/Clash.Verge_${version}_arm64.deb?~archive=false&~name=clash-verge.deb" + 'local:///LICENSE' +) + +checksums=( + 'sha256:035c83ed14b16df1dd397e5d710b34bedd5d27beb6678549ceaeeadf9bc167ed' + 'sha256:5b886bd36073c4604b97926ad81ad26ea64b9172f47f014c127b6a05f80e37a9' +) + +checksums_arm64=( + 'sha256:598a5a852d7bf9dc40a976780ef2afc9a4e5bfe7b99533e5f956f9e2f9def72f' + 'sha256:5b886bd36073c4604b97926ad81ad26ea64b9172f47f014c127b6a05f80e37a9' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x clash-verge.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + for path in usr opt etc; do + if [[ -d "${srcdir}/extracted/${path}" ]]; then + mkdir -p "${pkgdir}/${path}" + cp -a "${srcdir}/extracted/${path}/." "${pkgdir}/${path}/" + fi + done + + chmod 755 \ + "${pkgdir}/usr/bin/clash-verge" \ + "${pkgdir}/usr/bin/clash-verge-service" \ + "${pkgdir}/usr/bin/clash-verge-service-install" \ + "${pkgdir}/usr/bin/clash-verge-service-uninstall" \ + "${pkgdir}/usr/bin/verge-mihomo" \ + "${pkgdir}/usr/bin/verge-mihomo-alpha" + + install-license "${srcdir}/LICENSE" "clash-verge-rev/LICENSE.nivora" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/usr/lib/Clash Verge/**/*" \ + "/usr/share/icons/**/*" +} diff --git a/clash-verge-rev/postinstall.sh b/clash-verge-rev/postinstall.sh new file mode 100755 index 0000000..2462783 --- /dev/null +++ b/clash-verge-rev/postinstall.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi diff --git a/clash-verge-rev/postremove.sh b/clash-verge-rev/postremove.sh new file mode 100755 index 0000000..2462783 --- /dev/null +++ b/clash-verge-rev/postremove.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi diff --git a/clash-verge-rev/stapler-repo.toml b/clash-verge-rev/stapler-repo.toml new file mode 100644 index 0000000..d35f9bf --- /dev/null +++ b/clash-verge-rev/stapler-repo.toml @@ -0,0 +1 @@ +[inherit] diff --git a/claude-desktop/.stapler/update-check b/claude-desktop/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/claude-desktop/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/claude-desktop/LICENSE b/claude-desktop/LICENSE new file mode 100644 index 0000000..8e98cf1 --- /dev/null +++ b/claude-desktop/LICENSE @@ -0,0 +1,26 @@ +Сведения о лицензии пакета Claude + +Лицензия: Proprietary. +Исходный продукт: https://claude.ai/download +Документация desktop-приложения Claude: https://code.claude.com/docs/en/desktop-quickstart +Официальный Linux-репозиторий: https://downloads.claude.ai/claude-desktop/apt/stable + +Нюансы пакета: +- Пакет перепаковывает официальный Linux deb-артефакт `claude-desktop` из APT-репозитория Anthropic. +- В пакет входит десктопное Electron-приложение Claude с поддержкой сценариев Claude Code. +- Команда `claude-desktop` запускает основное desktop-приложение. +- Для одновременной работы с двумя аккаунтами пакет добавляет самостоятельное приложение `ClaudeAlt` с командами `claude-alt` / `claude-code-alt`, отдельной app-иконкой, executable/resources tree и Electron-идентичностью `com.anthropic.ClaudeAlt`. +- Tray-иконки основного Claude и ClaudeAlt являются перекрашенными вариантами штатного глифа Anthropic и регулируются теми же проприетарными условиями, что и исходный продукт. +- ClaudeAlt использует отдельный каталог web-данных `${XDG_CONFIG_HOME:-~/.config}/ClaudeAlt`, чтобы не смешивать cookies, авторизацию, OAuth-кэш, IndexedDB, Local Storage и Chromium lock-файлы. При обновлении существующий `${XDG_CONFIG_HOME:-~/.config}/Claude-Account-2` продолжает использоваться для сохранения авторизации. +- Путь профиля можно переопределить через `CLAUDE_ALT_DATA_DIR`; прежняя переменная `CLAUDE_DESKTOP_ACCOUNT2_DIR` поддерживается для совместимости. +- Глобальная память и конфигурация Claude Code в `~/.claude` могут использоваться обоими приложениями, поскольку этот путь принадлежит Claude Code, а не Electron-профилю. +- Облачные чаты и память Claude не объединяются пакетом: эти данные принадлежат конкретной учетной записи и хранятся на стороне Anthropic. +- Команда `claude` намеренно не добавляется, чтобы не конфликтовать с отдельным Claude Code CLI. +- Для полноценной работы требуется учетная запись Anthropic и подходящая подписка или доступ к сервису Claude. +- Настройки, авторизация, история, рабочие каталоги и данные проектов создаются приложением после запуска и не поставляются внутри пакета. +- Функции, зависящие от виртуализации или интеграции окружения, могут требовать дополнительные системные компоненты: QEMU, virtiofsd, portal backend, keyring/kwallet и аудиоподсистему. +- Исходный продукт является проприетарным. Использование Claude, Claude Code, сетевых функций, моделей и учетной записи регулируется юридическими условиями Anthropic. +- Внутри upstream-пакета также поставляются компоненты Electron/Chromium и virtiofsd со своими лицензиями; их тексты устанавливаются в `/usr/share/licenses/claude-desktop`. + +Официальные условия: +https://www.anthropic.com/legal/consumer-terms diff --git a/claude-desktop/Staplerfile b/claude-desktop/Staplerfile new file mode 100644 index 0000000..142ffaf --- /dev/null +++ b/claude-desktop/Staplerfile @@ -0,0 +1,388 @@ +name='claude-desktop' +version=1.24012.0 +release=4 +summary='Claude desktop application' +summary_ru='Десктопное приложение Claude' +group='Development/Tools' +desc='Claude is the Anthropic desktop application for Claude and Claude Code workflows with chat, project context, integrated terminal, editor and visual diff review.' +desc_ru='Claude — десктопное приложение Anthropic для Claude и сценариев Claude Code: чат, контекст проекта, встроенный терминал, редактор и визуальный просмотр diff.' +homepage='https://code.claude.com/docs/en/desktop-quickstart' +maintainer='Anthropic PBC ' + +architectures=('amd64' 'arm64') + +license=('Custom') +nonfree=1 +nonfree_msg='Claude Desktop является проприетарным приложением Anthropic. Использование Claude, Claude Code, учетной записи, подписки, сетевых функций и моделей регулируется условиями Anthropic.' +nonfree_url='https://www.anthropic.com/legal/consumer-terms' + +provides=() +replaces=('claude-desktop') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('libgtk-3-0' 'libnotify4' 'libnss3' 'xdg-utils' 'libatspi2.0-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libsecret-1-0' 'libc6' 'libxtst6' 'libuuid1' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk | xdg-desktop-portal-gnome | xdg-desktop-portal-kde') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'mesa-libgbm' 'libxcb' 'libsecret' 'glibc' 'libXtst' 'libuuid' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk') +deps_arch=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'libgbm' 'libxcb' 'libsecret' 'glibc' 'libxtst' 'libutil-linux' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk') +deps_opensuse=('gtk3' 'libnotify4' 'mozilla-nss' 'xdg-utils' 'at-spi2-core' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libsecret-1-0' 'glibc' 'libXtst6' 'libuuid1' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk') +deps_altlinux=('libgtk+3' 'libnotify' 'libnss' 'xdg-utils' 'libat-spi2-core' 'libdrm' 'libgbm' 'libxcb' 'libsecret' 'glibc' 'libXtst' 'libuuid' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk') + +opt_deps=() +opt_deps_debian=('libasound2t64 | libasound2 | pulseaudio' 'libayatana-appindicator3-1 | libappindicator3-1' 'ca-certificates' 'gnome-keyring | kwalletd6 | kwalletd5' 'qemu-system-x86' 'ovmf' 'virtiofsd') +opt_deps_ubuntu=("${opt_deps_debian[@]}") +opt_deps_fedora=('alsa-lib' 'pulseaudio-libs' 'libappindicator-gtk3' 'ca-certificates' 'gnome-keyring' 'kwallet' 'qemu-system-x86' 'edk2-ovmf' 'virtiofsd') +opt_deps_arch=('alsa-lib' 'pulseaudio' 'libappindicator-gtk3' 'ca-certificates' 'gnome-keyring' 'kwallet' 'qemu-system-x86' 'edk2-ovmf' 'virtiofsd') +opt_deps_opensuse=('libasound2' 'libpulse0' 'libappindicator3-1' 'ca-certificates' 'gnome-keyring' 'kwallet6' 'qemu-x86' 'ovmf' 'virtiofsd') +opt_deps_altlinux=('libalsa' 'libappindicator-gtk3' 'ca-certificates' 'gnome-keyring' 'qemu-system-x86' 'virtiofsd') + +build_deps=('binutils') +build_deps_debian=('binutils' 'python3') +build_deps_ubuntu=("${build_deps_debian[@]}") +build_deps_fedora=('binutils' 'python3') +build_deps_arch=('binutils' 'python') +build_deps_opensuse=('binutils' 'python3') +build_deps_altlinux=('binutils' 'python3') + +sources=( + "https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${version}_amd64.deb?~archive=false&~name=claude-desktop.deb" + "local:///claude-alt" + "local:///claude-alt.png" + "local:///claude-tray-orange.png" + "local:///claude-alt-tray-turquoise.png" + 'local:///LICENSE' +) + +sources_arm64=( + "https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${version}_arm64.deb?~archive=false&~name=claude-desktop.deb" + "local:///claude-alt" + "local:///claude-alt.png" + "local:///claude-tray-orange.png" + "local:///claude-alt-tray-turquoise.png" + 'local:///LICENSE' +) + +checksums=( + 'sha256:1096a8063956f8430fdbe504bb1c5e02985f31d0f45d082e4966e5d30ea7e3ac' + 'sha256:00d9fe6549d82f564a99c6e8ec54d1380fdf8f0a20c99bb9b3cda1f689adf57d' + 'sha256:f23de36a74c3f6ddae92511fc0dc659816853bf1d28520a6e0232fe2606fd0b4' + 'sha256:be51081c0e0b2eec3489c25da38194dccf68fe7445928b32c30e1b8051eb557b' + 'sha256:21d1db80c30921346252ed405fa36cb5c48c287afdd8ebe474073389fd88e85e' + 'sha256:40efe7a2755d02c9fe13f98cae18705e2d31e5bbb24816ebbd4951c5e3a56b3f' +) + +checksums_arm64=( + 'sha256:37dd5dc1b0bfcb3192561acbd33cb59037806fe9b95221a3ddf38244203196f1' + 'sha256:00d9fe6549d82f564a99c6e8ec54d1380fdf8f0a20c99bb9b3cda1f689adf57d' + 'sha256:f23de36a74c3f6ddae92511fc0dc659816853bf1d28520a6e0232fe2606fd0b4' + 'sha256:be51081c0e0b2eec3489c25da38194dccf68fe7445928b32c30e1b8051eb557b' + 'sha256:21d1db80c30921346252ed405fa36cb5c48c287afdd8ebe474073389fd88e85e' + 'sha256:40efe7a2755d02c9fe13f98cae18705e2d31e5bbb24816ebbd4951c5e3a56b3f' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x claude-desktop.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + install -dm755 "${pkgdir}/usr/lib" + cp -a "${srcdir}/extracted/usr/lib/claude-desktop" "${pkgdir}/usr/lib/" + chmod 0755 "${pkgdir}/usr/lib/claude-desktop/chrome-sandbox" + chmod 755 \ + "${pkgdir}/usr/lib/claude-desktop/claude-desktop" \ + "${pkgdir}/usr/lib/claude-desktop/resources/chrome-native-host" \ + "${pkgdir}/usr/lib/claude-desktop/resources/cowork-linux-helper" \ + "${pkgdir}/usr/lib/claude-desktop/resources/virtiofsd" + + # Сохраняем штатный tray-глиф Claude, меняя только его цвет. + for icon_name in TrayIconLinux.png TrayIconLinux-Dark.png; do + install -Dm644 "${srcdir}/claude-tray-orange.png" \ + "${pkgdir}/usr/lib/claude-desktop/resources/${icon_name}" + done + + # ClaudeAlt получает отдельные executable и app.asar. Остальные неизменяемые + # Electron-файлы подключаются к основной копии относительными symlink-ами. + # Это сохраняет отдельный resourcesPath, но не удваивает пакет целиком. + install -dm755 \ + "${pkgdir}/usr/lib/claude-alt" \ + "${pkgdir}/usr/lib/claude-alt/resources" + cp -p "${pkgdir}/usr/lib/claude-desktop/claude-desktop" \ + "${pkgdir}/usr/lib/claude-alt/claude-alt-bin" + + for entry in "${pkgdir}/usr/lib/claude-desktop"/*; do + entry_name="${entry##*/}" + case "${entry_name}" in + claude-desktop | resources) continue ;; + esac + ln -s "../claude-desktop/${entry_name}" \ + "${pkgdir}/usr/lib/claude-alt/${entry_name}" + done + + for entry in "${pkgdir}/usr/lib/claude-desktop/resources"/*; do + entry_name="${entry##*/}" + case "${entry_name}" in + app.asar) + cp -p "${entry}" "${pkgdir}/usr/lib/claude-alt/resources/app.asar" + ;; + icon.png | TrayIconLinux.png | TrayIconLinux-Dark.png) + # Эти ресурсы ниже заменяются иконкой ClaudeAlt. + ;; + *) + ln -s "../../claude-desktop/resources/${entry_name}" \ + "${pkgdir}/usr/lib/claude-alt/resources/${entry_name}" + ;; + esac + done + + # Большая иконка ClaudeAlt используется окном, а в tray устанавливается + # отдельный бирюзовый вариант штатного глифа Claude. + install -Dm644 "${srcdir}/claude-alt.png" \ + "${pkgdir}/usr/lib/claude-alt/resources/icon.png" + for icon_name in TrayIconLinux.png TrayIconLinux-Dark.png; do + install -Dm644 "${srcdir}/claude-alt-tray-turquoise.png" \ + "${pkgdir}/usr/lib/claude-alt/resources/${icon_name}" + done + + alt_asar="${pkgdir}/usr/lib/claude-alt/resources/app.asar" + + python3 - "${alt_asar}" <<'PY' +import copy +import hashlib +import json +import struct +import sys +from pathlib import Path + +asar_path = Path(sys.argv[1]) +data = asar_path.read_bytes() +if len(data) < 16: + raise SystemExit("ClaudeAlt app.asar is too small") + +_, _, _, header_size = struct.unpack(" dict: + blocks = [ + hashlib.sha256(content[index : index + block_size]).hexdigest() + for index in range(0, len(content), block_size) + ] + if not blocks: + blocks = [hashlib.sha256(b"").hexdigest()] + return { + "algorithm": "SHA256", + "hash": hashlib.sha256(content).hexdigest(), + "blockSize": block_size, + "blocks": blocks, + } + + +def rebuild_node(node: dict, prefix: str = "") -> dict: + global patched_cowork_socket, patched_package_json, patched_user_agent + + rebuilt = {} + for name, child in node.get("files", {}).items(): + path = f"{prefix}/{name}" if prefix else name + if "files" in child: + rebuilt[name] = { + **{key: value for key, value in child.items() if key != "files"}, + "files": rebuild_node(child, path), + } + continue + + entry = copy.deepcopy(child) + if entry.get("unpacked"): + rebuilt[name] = entry + continue + + offset = int(entry["offset"]) + size = int(entry["size"]) + content = data[base_offset + offset : base_offset + offset + size] + content_changed = False + + if path == "package.json": + package_json = json.loads(content) + expected = { + "productName": "Claude", + "desktopName": "com.anthropic.Claude.desktop", + } + for field, expected_value in expected.items(): + if package_json.get(field) != expected_value: + raise SystemExit( + f"unexpected Claude package.json {field}: " + f"{package_json.get(field)!r} != {expected_value!r}" + ) + # Отдельный productName нужен Electron для независимого instance + # lock. Официальный User-Agent восстанавливается ниже отдельно. + package_json["productName"] = "ClaudeAlt" + package_json["desktopName"] = "com.anthropic.ClaudeAlt.desktop" + content = (json.dumps(package_json, indent=2) + "\n").encode("utf-8") + patched_package_json += 1 + content_changed = True + + socket_name = b"claude-cowork-vm.sock" + socket_matches = content.count(socket_name) + if socket_matches: + content = content.replace(socket_name, b"claude-alt-cowork-vm.sock") + patched_cowork_socket += socket_matches + content_changed = True + + user_agent_marker = ( + b'P.app.isPackaged||P.app.setName("Claude");RRn();' + ) + user_agent_matches = content.count(user_agent_marker) + if user_agent_matches: + content = content.replace( + user_agent_marker, + b'P.app.isPackaged||P.app.setName("Claude");' + b'P.app.userAgentFallback=P.app.userAgentFallback' + b'.replaceAll("ClaudeAlt","Claude");RRn();', + ) + patched_user_agent += user_agent_matches + content_changed = True + + entry["offset"] = str(len(output)) + if content_changed: + entry["size"] = len(content) + entry["integrity"] = integrity_for( + content, + entry.get("integrity", {}).get("blockSize", 4194304), + ) + output.extend(content) + rebuilt[name] = entry + + return rebuilt + + +rebuilt_header = {"files": rebuild_node(header)} +if patched_package_json != 1: + raise SystemExit( + f"expected one ClaudeAlt package.json, got {patched_package_json}" + ) +if patched_cowork_socket != 1: + raise SystemExit( + f"expected one ClaudeAlt Cowork socket, got {patched_cowork_socket}" + ) +if patched_user_agent != 1: + raise SystemExit( + f"expected one ClaudeAlt User-Agent patch, got {patched_user_agent}" + ) + +header_bytes = json.dumps(rebuilt_header, separators=(",", ":")).encode("utf-8") +header_padded_size = (len(header_bytes) + 3) & ~3 +patched_data = ( + struct.pack( + "&2 + return 1 + } + sed -i \ + -e '0,/^Name=/{s/^Name=.*/Name=Claude\nName[ru]=Claude/}' \ + -e 's/^Categories=.*/Categories=Development;/' \ + "${desktop_file}" + + install -Dm644 "${srcdir}/claude-alt.png" \ + "${pkgdir}/usr/share/icons/hicolor/512x512/apps/claude-alt.png" + + install -Dm644 /dev/stdin "${pkgdir}/usr/share/applications/com.anthropic.ClaudeAlt.desktop" <<'EOF' +[Desktop Entry] +Name=ClaudeAlt +Name[ru]=ClaudeAlt +Comment=Independent secondary Claude desktop application +Comment[ru]=Независимое второе приложение Claude +GenericName=AI Assistant +Keywords=AI;Chat;Assistant;Claude;Code;LLM; +Exec=claude-alt %U +Icon=claude-alt +Type=Application +StartupNotify=true +StartupWMClass=com.anthropic.ClaudeAlt +SingleMainWindow=true +Categories=Development; +Actions=NewChat;NewCode; + +[Desktop Action NewChat] +Name=New chat +Name[ru]=Новый чат +Exec=claude-alt claude://claude.ai/new + +[Desktop Action NewCode] +Name=New Claude Code session +Name[ru]=Новая сессия Claude Code +Exec=claude-alt claude://code/new +EOF + + install -dm755 "${pkgdir}/usr/bin" + ln -sfn /usr/lib/claude-desktop/claude-desktop "${pkgdir}/usr/bin/claude-desktop" + ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-alt" + ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-code-alt" + ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-desktop-account2" + + install -dm755 "${pkgdir}/usr/share/licenses/claude-desktop" + install-license "${srcdir}/LICENSE" "claude-desktop/LICENSE.nivora" + install -Dm644 "${srcdir}/extracted/usr/share/doc/claude-desktop/copyright" \ + "${pkgdir}/usr/share/licenses/claude-desktop/copyright" + install -Dm644 "${srcdir}/extracted/usr/lib/claude-desktop/LICENSES.chromium.html" \ + "${pkgdir}/usr/share/licenses/claude-desktop/LICENSES.chromium.html" + install -Dm644 "${srcdir}/extracted/usr/lib/claude-desktop/resources/virtiofsd.LICENSE-APACHE" \ + "${pkgdir}/usr/share/licenses/claude-desktop/virtiofsd.LICENSE-APACHE" + install -Dm644 "${srcdir}/extracted/usr/lib/claude-desktop/resources/virtiofsd.LICENSE-BSD-3-Clause" \ + "${pkgdir}/usr/share/licenses/claude-desktop/virtiofsd.LICENSE-BSD-3-Clause" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/usr/lib/claude-desktop/**/*" \ + "/usr/lib/claude-alt/**/*" \ + "/usr/share/icons/hicolor/*/apps/claude-desktop.png" \ + "/usr/share/icons/hicolor/*/apps/claude-alt.png" +} diff --git a/claude-desktop/claude-alt b/claude-desktop/claude-alt new file mode 100755 index 0000000..a23848b --- /dev/null +++ b/claude-desktop/claude-alt @@ -0,0 +1,40 @@ +#!/bin/sh +set -eu + +if [ -z "${HOME:-}" ] && [ -z "${XDG_CONFIG_HOME:-}" ]; then + echo "claude-alt: HOME or XDG_CONFIG_HOME is required" >&2 + exit 1 +fi + +config_home="${XDG_CONFIG_HOME:-${HOME}/.config}" +default_data_dir="${config_home}/ClaudeAlt" +legacy_data_dir="${CLAUDE_DESKTOP_ACCOUNT2_DIR:-${config_home}/Claude-Account-2}" +claude_alt_executable="${CLAUDE_ALT_EXECUTABLE:-/usr/lib/claude-alt/claude-alt-bin}" + +if [ -n "${CLAUDE_ALT_DATA_DIR:-}" ]; then + data_dir="${CLAUDE_ALT_DATA_DIR}" +elif [ -d "${default_data_dir}" ]; then + data_dir="${default_data_dir}" +elif [ -d "${legacy_data_dir}" ]; then + # Сохраняем авторизацию прежнего второго профиля после обновления пакета. + data_dir="${legacy_data_dir}" +else + data_dir="${default_data_dir}" +fi + +if [ ! -x "${claude_alt_executable}" ]; then + echo "claude-alt: executable not found: ${claude_alt_executable}" >&2 + exit 1 +fi + +mkdir -p "${data_dir}" + +# Electron использует это имя для portal-интеграций. Основной Wayland app_id +# задаётся отдельным desktopName внутри ClaudeAlt app.asar. +CHROME_DESKTOP='com.anthropic.ClaudeAlt.desktop' +export CHROME_DESKTOP + +exec "${claude_alt_executable}" \ + --user-data-dir="${data_dir}" \ + --class=com.anthropic.ClaudeAlt \ + "$@" diff --git a/claude-desktop/claude-alt-tray-turquoise.png b/claude-desktop/claude-alt-tray-turquoise.png new file mode 100644 index 0000000..a650109 Binary files /dev/null and b/claude-desktop/claude-alt-tray-turquoise.png differ diff --git a/claude-desktop/claude-alt.png b/claude-desktop/claude-alt.png new file mode 100644 index 0000000..aa38a42 Binary files /dev/null and b/claude-desktop/claude-alt.png differ diff --git a/claude-desktop/claude-tray-orange.png b/claude-desktop/claude-tray-orange.png new file mode 100644 index 0000000..9900a9f Binary files /dev/null and b/claude-desktop/claude-tray-orange.png differ diff --git a/claude-desktop/postinstall.sh b/claude-desktop/postinstall.sh new file mode 100755 index 0000000..fc73778 --- /dev/null +++ b/claude-desktop/postinstall.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +sandbox=/usr/lib/claude-desktop/chrome-sandbox +if [[ ! -f "$sandbox" ]]; then + echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2 + exit 1 +fi +if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \ + && unshare --user true 2>/dev/null; then + chmod 0755 "$sandbox" +else + chmod 4755 "$sandbox" +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/claude-desktop/postremove.sh b/claude-desktop/postremove.sh new file mode 100755 index 0000000..44d2528 --- /dev/null +++ b/claude-desktop/postremove.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/claude-desktop/stapler-repo.toml b/claude-desktop/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/claude-desktop/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/codex/.stapler/update-check b/codex/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/codex/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/codex/LICENSE b/codex/LICENSE new file mode 100644 index 0000000..497e48e --- /dev/null +++ b/codex/LICENSE @@ -0,0 +1,21 @@ +Сведения о лицензии пакета codex-app + +Лицензия: Custom. +Источник перепаковки: https://github.com/Boria138/codex-app-linux +Linux-адаптация: https://github.com/ilysenko/codex-desktop-linux +Backend Computer Use: https://github.com/agent-sh/computer-use-linux + +Нюансы пакета: +- Пакет устанавливает неофициальную Linux-перепаковку проприетарного настольного приложения. +- Исходный репозиторий перепаковки не публикует единую корневую лицензию для всего устанавливаемого артефакта. +- Внутри архива исходного проекта присутствуют отдельные лицензии Electron, Chromium и других компонентов; при сборке они устанавливаются в каталог лицензий пакета. +- Linux-патчи Computer Use из codex-desktop-linux и backend computer-use-linux + распространяются по лицензии MIT; их исходные файлы LICENSE устанавливаются + рядом с остальными лицензиями пакета. +- Использование приложения и сетевых функций регулируется условиями исходного сервиса. +- Пакет помечен как несвободный через `nonfree=1`. + +Справочные ссылки: +- https://github.com/Boria138/codex-app-linux +- https://github.com/ilysenko/codex-desktop-linux +- https://github.com/agent-sh/computer-use-linux diff --git a/codex/Staplerfile b/codex/Staplerfile new file mode 100644 index 0000000..e273085 --- /dev/null +++ b/codex/Staplerfile @@ -0,0 +1,176 @@ +name='codex' +version=26.721.31836 +release=1 +summary='Unofficial Linux repack of the Codex desktop app' +summary_ru='Неофициальная Linux-перепаковка десктопного приложения Codex' +group='Development/Tools' +desc='codex-app-linux repacks the Codex desktop application into Linux Electron artifacts and bundles a Linux codex CLI binary for local workflows.' +desc_ru='codex-app-linux перепаковывает десктопное приложение Codex в Linux-артефакты Electron и включает Linux-бинарник codex CLI для локальных сценариев.' +homepage='https://github.com/Boria138/codex-app-linux' +maintainer='Boria138 ' + +architectures=('amd64') + +license=('Custom') +nonfree=1 +nonfree_msg='codex-app устанавливает неофициальную Linux-перепаковку проприетарного настольного приложения. Использование приложения и сетевых функций регулируется условиями исходного сервиса.' +nonfree_url='https://github.com/Boria138/codex-app-linux' + +provides=() +replaces=('codex') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('ca-certificates' 'libc6' 'libasound2' 'libatk-bridge2.0-0' 'libatk1.0-0' 'libatspi2.0-0' 'libcairo2' 'libcups2' 'libcurl3-gnutls | libcurl4' 'libdbus-1-3' 'libexpat1' 'libgbm1' 'libglib2.0-0' 'libgtk-3-0' 'libnspr4' 'libnss3' 'libpango-1.0-0' 'libsecret-1-0' 'libudev1' 'libx11-6' 'libxcb1' 'libxcomposite1' 'libxdamage1' 'libxext6' 'libxfixes3' 'libxkbcommon0' 'libxkbfile1' 'libxrandr2' 'libxss1' 'libxtst6' 'xdg-desktop-portal' 'xdg-utils') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('ca-certificates' 'glibc' 'alsa-lib' 'atk' 'at-spi2-atk' 'at-spi2-core' 'cairo' 'cups-libs' 'libcurl' 'dbus-libs' 'expat' 'mesa-libgbm' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'libsecret' 'systemd-libs' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' 'libxkbcommon' 'libxkbfile' 'libXrandr' 'libXScrnSaver' 'libXtst' 'xdg-desktop-portal' 'xdg-utils') +deps_arch=('ca-certificates' 'glibc' 'alsa-lib' 'atk' 'at-spi2-atk' 'at-spi2-core' 'cairo' 'cups' 'curl' 'dbus' 'expat' 'libgbm' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'libsecret' 'systemd-libs' 'libx11' 'libxcb' 'libxcomposite' 'libxdamage' 'libxext' 'libxfixes' 'libxkbcommon' 'libxkbfile' 'libxrandr' 'libxss' 'libxtst' 'xdg-desktop-portal' 'xdg-utils') +deps_opensuse=('ca-certificates' 'glibc' 'alsa' 'libatk-1_0-0' 'libatk-bridge-2_0-0' 'libatspi0' 'libcairo2' 'cups-libs' 'libcurl4' 'libdbus-1-3' 'libexpat1' 'libgbm1' 'glib2' 'gtk3' 'mozilla-nspr' 'mozilla-nss' 'pango' 'libsecret-1-0' 'libudev1' 'libX11-6' 'libxcb1' 'libXcomposite1' 'libXdamage1' 'libXext6' 'libXfixes3' 'libxkbcommon0' 'libxkbfile1' 'libXrandr2' 'libXss1' 'libXtst6' 'xdg-desktop-portal' 'xdg-utils') +deps_altlinux=('ca-certificates' 'glibc' 'libalsa' 'at-spi2-atk' 'libat-spi2-core' 'libatk' 'libcairo' 'libcups' 'libcurl' 'libdbus' 'libexpat' 'libgbm' 'glib2' 'libgtk+3' 'libnspr' 'libnss' 'libpango' 'libsecret' 'libudev0' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' 'libxkbcommon' 'libxkbfile' 'libXrandr' 'libXScrnSaver' 'libXtst' 'xdg-desktop-portal' 'xdg-utils') + +build_deps=('binutils') +build_deps_debian=('binutils' 'build-essential' 'cargo' 'libwayland-dev' 'nodejs' 'pkg-config' 'python3' 'rustc') +build_deps_ubuntu=("${build_deps_debian[@]}") +build_deps_fedora=('binutils' 'cargo' 'gcc' 'gcc-c++' 'make' 'nodejs' 'pkgconf-pkg-config' 'python3' 'rust' 'wayland-devel') +build_deps_arch=('base-devel' 'binutils' 'cargo' 'nodejs' 'pkgconf' 'python' 'rust' 'wayland') +build_deps_opensuse=('binutils' 'cargo' 'gcc' 'gcc-c++' 'make' 'nodejs' 'pkg-config' 'python3' 'rust' 'wayland-devel') +build_deps_altlinux=('binutils' 'gcc-c++' 'libwayland-client-devel' 'make' 'node' 'pkg-config' 'python3' 'rust' 'rust-cargo') + +sources=( + "https://github.com/Boria138/codex-app-linux/releases/download/${version}/codex-app-${version}-x86_64.tar.gz?~archive=false&~name=codex-app.tar.gz" + 'https://github.com/ilysenko/codex-desktop-linux/archive/15f3bd4fabc44bc0afba85b16d0121f0b4abd052.tar.gz?~archive=false&~name=codex-linux-port.tar.gz' + 'https://github.com/agent-sh/computer-use-linux/archive/refs/tags/v0.4.1.tar.gz?~archive=false&~name=computer-use-linux.tar.gz' + 'local:///asar_tree.py' + 'local:///patch-computer-use.js' + "local:///codex-app.png" + 'local:///LICENSE' +) + +checksums=( + 'sha256:f48c0ae3fca556d66525b863dd009a0bd0702115517f83c39f8a44cf4d128eb3' + 'sha256:8cde2dac642947cdfab23d302056ef81b574cc7f5684f25f7116f80411ec551c' + 'sha256:fab36489be4f1e92591d2ebcd28a96a95f788b5d1ba87824985623ab7df5e3f4' + 'sha256:9259dc4b9ae217855d55414b2266db08d9b40c5b0cdbca20a183234ff55ba006' + 'sha256:91b3cc2a6b660e3962f24976a29a261dae9a38f879758e6e6463e1295477e172' + 'sha256:5c4fe03d5777a9684e37ec9a2331302abf525f53db83762e6adaafd2fc5eb510' + 'sha256:755f407295f07facd26c968b73a204db5a6f25deac890d230062b09c33eb395e' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + tar -xzf codex-app.tar.gz + tar -xzf codex-linux-port.tar.gz + tar -xzf computer-use-linux.tar.gz + src="$(find "${srcdir}" -maxdepth 1 -type d -name "codex-app-${version}-x64" | head -1)" + linux_port_src="$(find "${srcdir}" -maxdepth 1 -type d -name 'codex-desktop-linux-*' | head -1)" + computer_use_src="$(find "${srcdir}" -maxdepth 1 -type d -name 'computer-use-linux-*' | head -1)" + + cargo build \ + --manifest-path "${computer_use_src}/Cargo.toml" \ + --target-dir "${srcdir}/computer-use-target" \ + --release \ + --locked + + install -dm755 "${pkgdir}/opt/codex-app" + cp -a "${src}/." "${pkgdir}/opt/codex-app/" + + codex_asar="${pkgdir}/opt/codex-app/resources/codex.asar" + extracted_asar="${srcdir}/codex-asar" + python3 "${srcdir}/asar_tree.py" extract "${codex_asar}" "${extracted_asar}" + node "${srcdir}/patch-computer-use.js" "${linux_port_src}" "${extracted_asar}" + python3 "${srcdir}/asar_tree.py" rebuild "${codex_asar}" "${extracted_asar}" + + bundled_root="${pkgdir}/opt/codex-app/resources/plugins/openai-bundled" + plugin_dir="${bundled_root}/plugins/computer-use" + install -dm755 "${plugin_dir}" + cp -a \ + "${linux_port_src}/plugins/openai-bundled/plugins/computer-use/." \ + "${plugin_dir}/" + find "${plugin_dir}" -name '.gitkeep' -delete + install -Dm755 \ + "${srcdir}/computer-use-target/release/computer-use-linux" \ + "${plugin_dir}/bin/codex-computer-use-linux" + install -Dm755 \ + "${srcdir}/computer-use-target/release/computer-use-linux-cosmic" \ + "${plugin_dir}/bin/codex-computer-use-cosmic" + install -Dm755 \ + "${srcdir}/computer-use-target/release/computer-use-linux-cosmic" \ + "${plugin_dir}/bin/computer-use-linux-cosmic" + install -Dm644 codex-app.png "${plugin_dir}/assets/app-icon.png" + + install -Dm644 /dev/stdin \ + "${bundled_root}/.agents/plugins/marketplace.json" <<'EOF' +{ + "plugins": [ + { + "name": "computer-use", + "source": { + "source": "local", + "path": "./plugins/computer-use" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} +EOF + + chmod 755 "${pkgdir}/opt/codex-app/codex-app" + chmod 755 "${pkgdir}/opt/codex-app/resources/codex" + if [[ -f "${pkgdir}/opt/codex-app/chrome-sandbox" ]]; then + chmod 0755 "${pkgdir}/opt/codex-app/chrome-sandbox" + fi + + install -dm755 "${pkgdir}/usr/bin" + ln -sfn /opt/codex-app/codex-app "${pkgdir}/usr/bin/codex-app" + ln -sfn \ + /opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/bin/codex-computer-use-linux \ + "${pkgdir}/usr/bin/codex-computer-use-linux" + + install -Dm644 codex-app.png \ + "${pkgdir}/usr/share/icons/hicolor/512x512/apps/codex-app.png" + + install -Dm644 "${src}/LICENSE.electron.txt" \ + "${pkgdir}/usr/share/licenses/codex/LICENSE.electron.txt" + install -Dm644 "${src}/LICENSES.chromium.html" \ + "${pkgdir}/usr/share/licenses/codex/LICENSES.chromium.html" + install -Dm644 "${linux_port_src}/LICENSE" \ + "${pkgdir}/usr/share/licenses/codex/LICENSE.codex-desktop-linux" + install -Dm644 "${computer_use_src}/LICENSE" \ + "${pkgdir}/usr/share/licenses/codex/LICENSE.computer-use-linux" + install-license "${srcdir}/LICENSE" "codex/LICENSE.nivora" + + install -Dm644 /dev/stdin "${pkgdir}/usr/share/applications/codex-app.desktop" <<'EOF' +[Desktop Entry] +Name=Codex +Comment=Desktop coding agent client +Comment[ru]=Десктопный клиент coding agent +Exec=/usr/bin/codex-app %U +Terminal=false +Type=Application +Icon=codex-app +Categories=Development;IDE; +StartupWMClass=codex-app +EOF +} + +files() { + files-find "/opt/codex-app/**/*" + files-find-binary + files-find-desktop + files-find-license + files-find "/usr/share/icons/hicolor/*/apps/codex-app.png" +} diff --git a/codex/asar_tree.py b/codex/asar_tree.py new file mode 100755 index 0000000..f9c2c19 --- /dev/null +++ b/codex/asar_tree.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +"""Extract and rebuild an Electron ASAR while preserving its metadata tree.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import struct +import tempfile +from pathlib import Path +from typing import Any, Iterator + + +class AsarError(RuntimeError): + pass + + +def align4(value: int) -> int: + return (value + 3) & ~3 + + +def read_archive(path: Path) -> tuple[dict[str, Any], bytes, int]: + payload = path.read_bytes() + if len(payload) < 16: + raise AsarError(f"{path}: truncated ASAR header") + + size_pickle_payload, header_pickle_size, header_payload_size, json_size = ( + struct.unpack_from(" len(payload) or 16 + json_size > data_offset: + raise AsarError(f"{path}: invalid ASAR header size") + + try: + header = json.loads(payload[16 : 16 + json_size].decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as error: + raise AsarError(f"{path}: invalid ASAR JSON header") from error + if not isinstance(header, dict) or not isinstance(header.get("files"), dict): + raise AsarError(f"{path}: ASAR header has no file tree") + return header, payload, data_offset + + +def safe_component(name: str) -> None: + if ( + not name + or name in {".", ".."} + or "/" in name + or "\\" in name + or "\0" in name + ): + raise AsarError(f"unsafe ASAR entry name: {name!r}") + + +def entries( + files: dict[str, Any], prefix: tuple[str, ...] = () +) -> Iterator[tuple[tuple[str, ...], dict[str, Any]]]: + for name, metadata in files.items(): + safe_component(name) + if not isinstance(metadata, dict): + raise AsarError(f"invalid metadata for {'/'.join((*prefix, name))}") + relative = (*prefix, name) + children = metadata.get("files") + if children is not None: + if not isinstance(children, dict): + raise AsarError(f"invalid directory entry: {'/'.join(relative)}") + yield from entries(children, relative) + else: + yield relative, metadata + + +def output_path(root: Path, relative: tuple[str, ...]) -> Path: + candidate = root.joinpath(*relative) + try: + candidate.resolve().relative_to(root.resolve()) + except ValueError as error: + raise AsarError(f"entry escapes extraction root: {'/'.join(relative)}") from error + return candidate + + +def extract(archive: Path, destination: Path) -> None: + header, payload, data_offset = read_archive(archive) + destination.mkdir(parents=True, exist_ok=True) + + for relative, metadata in entries(header["files"]): + if metadata.get("unpacked") is True or "link" in metadata: + continue + try: + size = int(metadata["size"]) + offset = int(metadata["offset"]) + except (KeyError, TypeError, ValueError) as error: + raise AsarError(f"invalid file entry: {'/'.join(relative)}") from error + start = data_offset + offset + end = start + size + if offset < 0 or size < 0 or end > len(payload): + raise AsarError(f"file lies outside archive: {'/'.join(relative)}") + + target = output_path(destination, relative) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(payload[start:end]) + if metadata.get("executable") is True: + target.chmod(target.stat().st_mode | 0o111) + + +def integrity(content: bytes, block_size: int) -> dict[str, Any]: + blocks = [ + hashlib.sha256(content[offset : offset + block_size]).hexdigest() + for offset in range(0, len(content), block_size) + ] + if not blocks: + blocks = [hashlib.sha256(b"").hexdigest()] + return { + "algorithm": "SHA256", + "hash": hashlib.sha256(content).hexdigest(), + "blockSize": block_size, + "blocks": blocks, + } + + +def rebuild(archive: Path, source: Path, output: Path) -> None: + header, _, _ = read_archive(archive) + content_chunks: list[bytes] = [] + offset = 0 + + for relative, metadata in entries(header["files"]): + if metadata.get("unpacked") is True or "link" in metadata: + continue + target = output_path(source, relative) + if not target.is_file(): + raise AsarError(f"missing extracted file: {'/'.join(relative)}") + content = target.read_bytes() + metadata["size"] = len(content) + metadata["offset"] = str(offset) + if "integrity" in metadata: + block_size = int(metadata["integrity"].get("blockSize", 4 * 1024 * 1024)) + metadata["integrity"] = integrity(content, block_size) + content_chunks.append(content) + offset += len(content) + + json_payload = json.dumps( + header, ensure_ascii=False, separators=(",", ":") + ).encode("utf-8") + padded_json_size = align4(len(json_payload)) + header_payload_size = 4 + padded_json_size + prefix = struct.pack( + " int: + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + + extract_parser = subparsers.add_parser("extract") + extract_parser.add_argument("archive", type=Path) + extract_parser.add_argument("destination", type=Path) + + rebuild_parser = subparsers.add_parser("rebuild") + rebuild_parser.add_argument("archive", type=Path) + rebuild_parser.add_argument("source", type=Path) + rebuild_parser.add_argument("output", type=Path, nargs="?") + + arguments = parser.parse_args() + if arguments.command == "extract": + extract(arguments.archive, arguments.destination) + else: + rebuild( + arguments.archive, + arguments.source, + arguments.output or arguments.archive, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/codex/codex-app.png b/codex/codex-app.png new file mode 100644 index 0000000..5084b8c Binary files /dev/null and b/codex/codex-app.png differ diff --git a/codex/patch-computer-use.js b/codex/patch-computer-use.js new file mode 100755 index 0000000..54fc503 --- /dev/null +++ b/codex/patch-computer-use.js @@ -0,0 +1,139 @@ +#!/usr/bin/env node +"use strict"; + +// This file only adapts upstream patch discovery to the current hashed bundle +// name. Computer Use behavior remains sourced from codex-desktop-linux. + +const fs = require("node:fs"); +const path = require("node:path"); + +const linuxPortRoot = path.resolve(process.argv[2] ?? ""); +const extractedApp = path.resolve(process.argv[3] ?? ""); +if (!fs.existsSync(linuxPortRoot) || !fs.existsSync(extractedApp)) { + console.error("usage: patch-computer-use.js LINUX_PORT_ROOT EXTRACTED_APP"); + process.exit(2); +} + +process.env.CODEX_LINUX_ENABLE_COMPUTER_USE_UI = "1"; + +const { patchExtractedApp } = require( + path.join(linuxPortRoot, "scripts/patches/runner.js"), +); +const implementation = require( + path.join(linuxPortRoot, "scripts/patches/impl/computer-use.js"), +); +const coreRoot = path.join( + linuxPortRoot, + "scripts/patches/core/all-linux", +); + +patchExtractedApp(extractedApp, { + corePatchRoot: path.join(coreRoot, "main-process/computer-use"), +}); +patchExtractedApp(extractedApp, { + corePatchRoot: path.join(coreRoot, "webview/computer-use-ui"), +}); + +const assetsDirectory = path.join(extractedApp, "webview/assets"); +const javaScriptAssets = fs.readdirSync(assetsDirectory) + .filter((name) => name.endsWith(".js")) + .map((name) => path.join(assetsDirectory, name)); + +function applyToUniqueAsset(label, patchFunction, alreadyPatched, isCandidate) { + const changed = []; + const existing = []; + for (const target of javaScriptAssets) { + const source = fs.readFileSync(target, "utf8"); + if (alreadyPatched(source)) { + existing.push(target); + continue; + } + if (!isCandidate(source)) { + continue; + } + const patched = patchFunction(source); + if (patched !== source) { + changed.push({ target, patched }); + } + } + if (existing.length === 1 && changed.length === 0) { + return; + } + if (existing.length !== 0 || changed.length !== 1) { + throw new Error( + `${label}: expected one target, found existing=${existing.length}, changed=${changed.length}`, + ); + } + fs.writeFileSync(changed[0].target, changed[0].patched, "utf8"); + console.log(`${label}: ${path.basename(changed[0].target)}`); +} + +applyToUniqueAsset( + "Linux Computer Use host platform", + implementation.applyLinuxComputerUseHostPlatformPatch, + (source) => + /featureName:`computer_use`[\s\S]{0,2200}?isHostCompatiblePlatform:[A-Za-z_$][\w$]*===`linux`\|\|/.test( + source, + ), + (source) => source.includes("featureName:`computer_use`"), +); +applyToUniqueAsset( + "Linux Computer Use install flow", + implementation.applyLinuxComputerUseInstallFlowPatch, + (source) => + source.includes("plugin detail query requires pluginName") && + /let [A-Za-z_$][\w$]*=[A-Za-z_$][\w$]*&&[A-Za-z_$][\w$]*!==`computer-use`,/.test( + source, + ), + (source) => source.includes("plugin detail query requires pluginName"), +); + +const iconCandidates = fs.readdirSync(assetsDirectory) + .filter((name) => /^computer-use-plugin-icon-[^.]+\.png$/.test(name)) + .filter((name) => name !== "computer-use-plugin-icon-linux.png"); +if (iconCandidates.length !== 1) { + throw new Error( + `Computer Use icon: expected one source, found ${iconCandidates.length}`, + ); +} +let iconReferences = 0; +for (const target of javaScriptAssets) { + const source = fs.readFileSync(target, "utf8"); + if (!source.includes("computer-use-plugin-icon-linux.png")) { + continue; + } + fs.writeFileSync( + target, + source.replaceAll( + "computer-use-plugin-icon-linux.png", + iconCandidates[0], + ), + "utf8", + ); + iconReferences += 1; +} +if (iconReferences !== 1) { + throw new Error( + `Computer Use icon: expected one patched reference, found ${iconReferences}`, + ); +} + +const mainBundle = fs.readdirSync(path.join(extractedApp, ".vite/build")) + .filter((name) => /^main-[^.]+\.js$/.test(name)); +if (mainBundle.length !== 1) { + throw new Error(`main bundle: expected one target, found ${mainBundle.length}`); +} +const mainSource = fs.readFileSync( + path.join(extractedApp, ".vite/build", mainBundle[0]), + "utf8", +); +for (const marker of [ + "codexLinuxNativeDesktopApps(", + "codexLinuxRegisterComputerUseCursorHandler", + "platform!==`darwin`&&", +]) { + if (!mainSource.includes(marker)) { + throw new Error(`main bundle: missing upstream marker ${marker}`); + } +} +console.log("Linux Computer Use upstream patches applied"); diff --git a/codex/postinstall.sh b/codex/postinstall.sh new file mode 100755 index 0000000..8392fb2 --- /dev/null +++ b/codex/postinstall.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +sandbox=/opt/codex-app/chrome-sandbox +if [[ ! -f "$sandbox" ]]; then + echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2 + exit 1 +fi +if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \ + && unshare --user true 2>/dev/null; then + chmod 0755 "$sandbox" +else + chmod 4755 "$sandbox" +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi + +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/codex/postremove.sh b/codex/postremove.sh new file mode 100755 index 0000000..8d4bf97 --- /dev/null +++ b/codex/postremove.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi + +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/codex/stapler-repo.toml b/codex/stapler-repo.toml new file mode 100644 index 0000000..d35f9bf --- /dev/null +++ b/codex/stapler-repo.toml @@ -0,0 +1 @@ +[inherit] diff --git a/docs/maintenance.md b/docs/maintenance.md new file mode 100644 index 0000000..4d5388a --- /dev/null +++ b/docs/maintenance.md @@ -0,0 +1,106 @@ +# Сопровождение Nivora + +## Инварианты + +- В репозитории ровно 19 каталогов с `Staplerfile`. +- Каталог совпадает с `name` и командой в README. +- Upstream-версия не меняется из-за патча рецепта; для этого повышается `release`. +- Desktop-id, AppStream component-id, units и пути данных не меняются без отдельной миграции. +- `provides` и `conflicts` остаются пустыми, а `replaces` содержит только собственное + базовое имя пакета. + +Stapler сам добавляет текущее `name` в generated `Provides` и `Conflicts`. Рецепты не +добавляют переходные package ID и не зависят от других Stapler-каталогов. + +## Названия + +| Package ID | Основание | +|:--|:--| +| `clash-verge-rev` | Upstream-репозиторий и release title: Clash Verge Rev | +| `claude-desktop` | Upstream DEB: `Package: claude-desktop`; desktop-id `com.anthropic.Claude` сохранён | +| `codex` | Отображаемое имя `Codex`; команда и desktop-id `codex-app` сохранены | +| `github-desktop` | Официальный upstream `desktop/desktop`; Linux-сборка без стороннего форка | +| `nivora-stplr` | Собственный helper Nivora | + +## Локальные проверки + +```bash +tools/run_checks.sh +tools/package_updates.sh check-all +tools/verify_artifacts.sh --all +tools/test_package_lifecycle.sh +``` + +`run_checks.sh` выполняет `bash -n`, ShellCheck, Python compile, unit-тесты, validator и чтение +всех `Staplerfile` через `stplr-spec`. + +`verify_artifacts.sh` сопоставляет готовые RPM с `files()`, проверяет владельцев путей, +права, desktop-файлы, systemd units, иконки, лицензии и метаданные совместимости. + +`test_package_lifecycle.sh` собирает настоящие DEB текущей версии и использует настоящие +RPM из clean-build. Минимальные fixtures изображают предыдущую версию того же пакета Nivora. +В одноразовых Ubuntu и ALT-контейнерах через APT проверяются: + +1. обновление с предыдущей версии Nivora на текущую; +2. `Provides`, `Replaces` и `Conflicts`; +3. наличие команды, desktop-файла или systemd unit; +4. сохранение пользовательского состояния после обновления и удаления. + +Полный жизненный цикл проверяется для восьми критичных пакетов. Остальные пакеты +покрываются validator, clean-build и проверкой payload. + +Локально DEB собираются в привилегированном контейнере. На GitHub-hosted runner используется +`NIVORA_DEB_BUILD_MODE=host`: закреплённый stplr запускается непосредственно на одноразовом +Ubuntu runner, потому что вложенный sandbox stplr запрещён внутри Docker. Ubuntu 24.04 может +дополнительно блокировать непривилегированные user namespaces через AppArmor: тест временно +снимает только это ограничение, проверяет полный набор namespaces перед сборкой и +восстанавливает исходное значение при завершении. Для совместимости с моделью привилегий +Stapler временный builder включается в группу `wheel`, отсутствующую в Ubuntu по умолчанию. +Транзакционные сценарии в обоих режимах остаются изолированными в контейнерах. + +## Обновление пакета + +```bash +stplr-spec update-package package +stplr-spec verify-checksums --path package/Staplerfile +tools/run_checks.sh +tools/clean_build.sh package +``` + +Нестандартная логика обнаружения версий находится в `tools/package_updates.sh`, а каждый +`.stapler/update-check` вызывает его для своего package ID. + +Плановый workflow обновляет пакеты автономно и отправляет проверенные изменения +прямо в `main`. Он запускается ежедневно в 03:00 по Владивостоку +(`17:00 UTC`). Каждый пакет обрабатывается в отдельном временном worktree, поэтому +несовместимое обновление одного upstream не блокирует остальные. При сбое workflow +сохраняет на 30 дней диагностический artifact с полным логом, фазой сбоя, diff и +получившимся `Staplerfile`; успешно собранные пакеты всё равно публикуются. Для +каждого несовместимого пакета создаётся один постоянный issue: повторные сбои +обновляют его, а успешное восстановление автоматически закрывает. Ожидаемый сбой +отдельного пакета помечается предупреждением и не делает весь этап обновления +неуспешным. + +## Clean-build + +```bash +tools/clean_build.sh package +tools/clean_build.sh --all +tools/verify_artifacts.sh --all +``` + +Скрипт всегда выполняет сборку в собственном одноразовом контейнере ALT и не +подключает сторонние Stapler-каталоги. + +## Проверка жизненного цикла + +Для каждого критичного пакета нужно: + +1. Собрать fixture предыдущей версии и текущий RPM/DEB. +2. Создать тестовый файл в каталоге данных. +3. Обновить пакет до текущей версии. +4. Проверить `Provides/Replaces/Conflicts`, payload и тестовый файл. +5. Проверить удаление пакета без удаления пользовательского состояния. + +Автоматизированная проверка выполняется командой `tools/test_package_lifecycle.sh` в +одноразовых контейнерах, а не на рабочей системе сопровождающего. diff --git a/docs/packages/claude-desktop.md b/docs/packages/claude-desktop.md new file mode 100644 index 0000000..d034845 --- /dev/null +++ b/docs/packages/claude-desktop.md @@ -0,0 +1,52 @@ +# Claude Desktop + +Пакет `claude-desktop` переупаковывает DEB из APT-источника Anthropic и сохраняет upstream +desktop-id `com.anthropic.Claude`. + +```bash +sudo stplr install nivora/claude-desktop +``` + +## Два независимых приложения + +- `claude-desktop` запускает основное приложение Claude. +- `claude-alt` и `claude-code-alt` запускают отдельное приложение ClaudeAlt. +- `claude-desktop-account2` — дополнительное имя запуска ClaudeAlt. +- Команда `claude` не создаётся, чтобы не конфликтовать с Claude Code CLI. + +ClaudeAlt имеет отдельные executable/resources tree, Electron `productName`, Wayland `app_id`, +X11 `WM_CLASS`, desktop-файл, Chromium-профиль, Cowork VM socket, оконную и +tray-иконки. В системном лотке используется штатный глиф Claude: оранжевый у +основного приложения и бирюзовый у ClaudeAlt. Большие app-иконки окон при этом +остаются отдельными. Поэтому окружение рабочего стола группирует Claude и ClaudeAlt +как разные приложения, а `requestSingleInstanceLock()` и Cowork runtime каждого +приложения работают независимо. + +Новая установка ClaudeAlt хранит cookies, OAuth, IndexedDB и Chromium locks в +`${XDG_CONFIG_HOME:-~/.config}/ClaudeAlt`. Если уже существует прежний каталог +`${XDG_CONFIG_HOME:-~/.config}/Claude-Account-2`, launcher использует его, чтобы после обновления +не потерять авторизацию второго аккаунта. Глобальные данные Claude Code в `~/.claude` могут +по-прежнему использоваться обоими приложениями. + +## Настройка + +| Переменная | Назначение | +|:--|:--| +| `CLAUDE_ALT_DATA_DIR` | Явно задать каталог профиля ClaudeAlt | +| `CLAUDE_DESKTOP_ACCOUNT2_DIR` | Совместимое имя переменной прежнего второго профиля | +| `CLAUDE_ALT_EXECUTABLE` | Переопределить executable ClaudeAlt для диагностики | + +Основной Claude продолжает использовать upstream desktop-id `com.anthropic.Claude`, а ClaudeAlt +использует `com.anthropic.ClaudeAlt`. + +## Сетевая изоляция + +Claude и ClaudeAlt имеют разные Chromium-профили, single-instance sockets, +cookies, browser storage, Crashpad-каталоги и Cowork VM sockets. Временные OAuth +callback-серверы по умолчанию получают свободный loopback-порт от ОС. + +Приложения не являются сетевыми песочницами: оба выходят в Интернет с +одного хоста и публичного IP-адреса. Общий `claude://` URL handler намеренно +остаётся за основным Claude. Если в обоих приложениях вручную задать +один и тот же фиксированный OAuth callback-порт и запустить авторизацию +одновременно, второе приложение получит `EADDRINUSE`. diff --git a/docs/packages/codex.md b/docs/packages/codex.md new file mode 100644 index 0000000..6e6a1ad --- /dev/null +++ b/docs/packages/codex.md @@ -0,0 +1,57 @@ +# Codex + +Пакет устанавливает неофициальную Linux-перепаковку Codex Desktop и +предусмотренную Linux-реализацию Computer Use из проектов +[`codex-desktop-linux`](https://github.com/ilysenko/codex-desktop-linux) и +[`computer-use-linux`](https://github.com/agent-sh/computer-use-linux). + +В пакет входят: + +- приложение `codex-app`; +- bundled plugin `computer-use@openai-bundled`; +- нативный backend `codex-computer-use-linux`, собранный из исходников для + версии glibc текущего дистрибутива; +- интеграция с AT-SPI, XDG Desktop Portal и доступными средствами ввода Linux. + +Патчи не включают серверную возможность в обход Codex. Они добавляют Linux в +список поддерживаемых локальных платформ и сохраняют проверку feature gate +`computer_use`, которую возвращает сервис. + +## Первичная настройка + +После установки полностью перезапустите Codex. Состояние backend проверяется +без изменения системы: + +```bash +codex-computer-use-linux doctor +``` + +Рекомендуемую настройку доступности и ввода выполняет upstream-мастер: + +```bash +codex-computer-use-linux setup +``` + +Для получения списка окон в GNOME может понадобиться отдельная настройка +расширения: + +```bash +codex-computer-use-linux setup-window-targeting +``` + +Команда `setup` идемпотентно включает AT-SPI в текущем пользовательском сеансе; +она изменяет пользовательскую настройку доступности без отдельного запроса. +Разрешения на захват экрана и управление вводом выдаются пользователем через +системные диалоги портала. + +## Диагностика + +```bash +codex-computer-use-linux doctor +codex-computer-use-linux apps +codex-computer-use-linux windows +``` + +Если интерфейс всё ещё показывает недоступность, сначала убедитесь, что +установлен пакет Nivora с `release=2` или новее, затем закройте все процессы +Codex и запустите приложение заново. diff --git a/docs/packages/github-desktop.md b/docs/packages/github-desktop.md new file mode 100644 index 0000000..58432d0 --- /dev/null +++ b/docs/packages/github-desktop.md @@ -0,0 +1,27 @@ +# GitHub Desktop + +Пакет собирает оригинальный GitHub Desktop для Linux непосредственно из +официального репозитория [`desktop/desktop`](https://github.com/desktop/desktop). +Готовые артефакты, патчи и исходники `shiftkey/desktop` либо других форков не +используются. + +GitHub официально выпускает установщики только для macOS и Windows. Linux-сборка +в Nivora поэтому является неофициальным пакетом, но код приложения и закреплённые +submodule-исходники берутся только из официальных репозиториев GitHub. + +Тяжёлая сборка выполняется workflow `Сборка GitHub Desktop для Linux` на +нативных GitHub-hosted runner’ах `x64` и `arm64`. Workflow публикует +детерминированно упакованные tar.gz и `SHA256SUMS` в релизе Nivora, а рецепт +скачивает эти готовые артефакты с закреплёнными контрольными суммами. + +Установка: + +```bash +sudo stplr install nivora/github-desktop +``` + +Команда запуска: + +```bash +github-desktop +``` diff --git a/docs/packages/nivora-stplr.md b/docs/packages/nivora-stplr.md new file mode 100644 index 0000000..30208c8 --- /dev/null +++ b/docs/packages/nivora-stplr.md @@ -0,0 +1,43 @@ +# Nivora Stapler Helper + +`nivora-stplr` добавляет короткие команды для Stapler. + +```bash +sudo stplr install nivora/nivora-stplr +``` + +## Команды + +| Alias | Вызов Stapler | +|:--|:--| +| `sli` | `sudo stplr install` | +| `slr` | `sudo stplr remove` | +| `slu` | `sudo stplr up` | +| `slf` | `sudo stplr fix` | +| `slref` | `sudo stplr refresh` | +| `sls` | `stplr search` | +| `slii` | `stplr info` | +| `sll` | `stplr list` | +| `sl` | Диспетчер тех же команд | + +Примеры: + +```bash +sli --repo nivora parsec +slii nivora/codex +slu --clean +``` + +Короткое имя пакета не привязывается к Nivora по умолчанию. Префикс задаётся явно: + +```bash +export NIVORA_STPLR_REPO=nivora +``` + +| Переменная | Назначение | +|:--|:--| +| `NIVORA_STPLR_REPO` | Префикс для коротких package names | +| `NIVORA_STPLR_SUDO` | Команда повышения привилегий, по умолчанию `sudo` | +| `NIVORA_STPLR_QUIET=1` | Не печатать команду перед запуском | + +Helper не передаёт произвольные shell-строки через `sudo`. diff --git a/docs/packages/opencode.md b/docs/packages/opencode.md new file mode 100644 index 0000000..d6bc8cf --- /dev/null +++ b/docs/packages/opencode.md @@ -0,0 +1,16 @@ +# OpenCode + +Пакет устанавливает официальный Linux desktop-артефакт OpenCode без изменений его кода и ресурсов. + +```bash +sudo stplr install nivora/opencode +``` + +## Запуск + +- из меню приложений — `OpenCode`; +- из терминала — `opencode-desktop`. + +Команда `opencode-desktop` является прямой ссылкой на upstream-бинарник +`/opt/OpenCode/ai.opencode.desktop`: wrapper, лимиты памяти, GPU-fallback и другие поведенческие +патчи не применяются. diff --git a/docs/packages/ventoy.md b/docs/packages/ventoy.md new file mode 100644 index 0000000..8cdb1ca --- /dev/null +++ b/docs/packages/ventoy.md @@ -0,0 +1,21 @@ +# Ventoy + +Пакет устанавливает официальный нативный Linux GUI Ventoy и показывает его в +меню приложений под коротким названием **Ventoy**. + +## Запуск + +```bash +ventoy +``` + +При запуске Ventoy запрашивает административные права через `pkexec`, потому +что программа размечает накопители и записывает загрузочные структуры напрямую. + +## Важно + +- Перед установкой Ventoy внимательно проверьте выбранное устройство. +- Первичная установка стирает данные на выбранном накопителе. +- Обновление уже подготовленного Ventoy-накопителя сохраняет раздел с образами, + однако резервная копия важных данных всё равно рекомендуется. +- Пакет использует официальный архив upstream и поддерживает `amd64` и `arm64`. diff --git a/docs/security-model.md b/docs/security-model.md new file mode 100644 index 0000000..8e0a628 --- /dev/null +++ b/docs/security-model.md @@ -0,0 +1,34 @@ +# Модель доверия + +Nivora — независимый community-репозиторий. Он не контролируется командой Stapler или +разработчиками упакованных приложений. + +## Что можно проверить + +- `Staplerfile` описывает sources, зависимости, сборку и состав payload. +- Локальные wrappers и hooks хранятся рядом с рецептом. +- Каждому source соответствует SHA-256. +- Валидатор запрещает HTTP, `SKIP`, отсутствующие локальные sources и незакреплённые Git URL. + +## Что не гарантирует SHA-256 + +Контрольная сумма обнаруживает замену файла после фиксации рецепта. Она не доказывает: + +- отсутствие уязвимостей в upstream-приложении; +- корректность сетевого сервиса; +- соответствие файла исходному коду без воспроизводимой upstream-сборки. + +## Проприетарные пакеты + +Для `Custom` и `Proprietary` Nivora переупаковывает опубликованный артефакт, но не изменяет +условия его владельца. Учётные записи, подписки, API и сетевые сервисы в пакет не входят. + +## Данные пользователя + +Пакетные scripts не должны удалять каталоги в `$HOME`, выполнять logout или сбрасывать учётную +запись при обычном удалении. Отдельные команды очистки Tailscale и NetBird требуют `--yes`. + +## Границы CI + +CI проверяет структуру, shell/Python, metadata, checksums и выбранные сборки. Разница в версиях +библиотек, драйверах, desktop-окружениях и внешних API остаётся ограничением. diff --git a/fisher/.stapler/update-check b/fisher/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/fisher/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/fisher/LICENSE b/fisher/LICENSE new file mode 100644 index 0000000..debbcb9 --- /dev/null +++ b/fisher/LICENSE @@ -0,0 +1,12 @@ +Сведения о лицензии пакета Fisher + +Лицензия: MIT. +Исходный проект: https://github.com/jorgebucaran/fisher + +Нюансы пакета: +- Пакет устанавливает Fisher — менеджер плагинов для Fish shell. +- Код распространяется на условиях MIT License. +- Пакет не добавляет закрытых бинарных компонентов. + +Полный официальный текст MIT: +https://opensource.org/licenses/MIT diff --git a/fisher/Staplerfile b/fisher/Staplerfile new file mode 100644 index 0000000..7cadf05 --- /dev/null +++ b/fisher/Staplerfile @@ -0,0 +1,54 @@ +name='fisher' +version='4.4.8' +release=3 +summary='Fish plugin manager' +summary_ru='Менеджер плагинов для Fish shell' +desc='Fisher is a plugin manager for Fish shell — zero configuration, pure Fish, blazing fast concurrent downloads.' +desc_ru='Fisher — менеджер плагинов для Fish shell. Ноль конфигурации, чистый Fish, молниеносная параллельная загрузка.' +group='Shells' +homepage='https://github.com/jorgebucaran/fisher' +maintainer='Cheviiot ' + +architectures=('all') + +license=('MIT') + +provides=() +replaces=('fisher') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=( + 'fish' +) + +deps_debian=('fish') +deps_ubuntu=('fish') +deps_fedora=('fish') +deps_arch=('fish') +deps_altlinux=('fish') +deps_opensuse=('fish') + +sources=( + "https://raw.githubusercontent.com/jorgebucaran/fisher/${version}/functions/fisher.fish" + 'local:///LICENSE' +) +checksums=( + '0fb6c81ae3003e95b5671766fa6c25c3597066e29965b7772f6c1b007387356d' + '567ce81f1514e7338d5c1a1e5c2d8ef772c1ff01d77d7b76501e67c148b14dbb' +) + +package() { + install -Dm644 "${srcdir}/fisher.fish" \ + "${pkgdir}/usr/share/fish/vendor_functions.d/fisher.fish" + install-license "${srcdir}/LICENSE" "fisher/LICENSE.nivora" +} + +files() { + files-find \ + "/usr/share/fish/vendor_functions.d/fisher.fish" + files-find-license +} diff --git a/fisher/stapler-repo.toml b/fisher/stapler-repo.toml new file mode 100644 index 0000000..d35f9bf --- /dev/null +++ b/fisher/stapler-repo.toml @@ -0,0 +1 @@ +[inherit] diff --git a/github-desktop/.stapler/update-check b/github-desktop/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/github-desktop/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/github-desktop/Staplerfile b/github-desktop/Staplerfile new file mode 100644 index 0000000..debdc6c --- /dev/null +++ b/github-desktop/Staplerfile @@ -0,0 +1,109 @@ +name='github-desktop' +version='3.6.3' +release=2 +summary='GitHub Desktop built from the official source' +summary_ru='GitHub Desktop, собранный из официальных исходников' +group='Development/Tools' +desc='The original GitHub Desktop application, built for Linux by Nivora CI directly from the official desktop/desktop source tree without third-party forks.' +desc_ru='Оригинальное приложение GitHub Desktop, собранное для Linux в CI Nivora напрямую из официального дерева desktop/desktop без сторонних форков.' +homepage='https://github.com/desktop/desktop' +maintainer='GitHub ' + +architectures=('amd64' 'arm64') + +license=('MIT') + +provides=() +replaces=('github-desktop') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('ca-certificates' 'git' 'libasound2' 'libatk-bridge2.0-0' 'libatk1.0-0' 'libatspi2.0-0' 'libcairo2' 'libcups2' 'libdbus-1-3' 'libexpat1' 'libgbm1' 'libglib2.0-0' 'libgtk-3-0' 'libnspr4' 'libnss3' 'libpango-1.0-0' 'libsecret-1-0' 'libudev1' 'libx11-6' 'libxcb1' 'libxcomposite1' 'libxdamage1' 'libxext6' 'libxfixes3' 'libxkbcommon0' 'libxrandr2' 'libxss1' 'libxtst6' 'xdg-utils') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('ca-certificates' 'git' 'alsa-lib' 'atk' 'at-spi2-atk' 'at-spi2-core' 'cairo' 'cups-libs' 'dbus-libs' 'expat' 'mesa-libgbm' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'libsecret' 'systemd-libs' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' 'libxkbcommon' 'libXrandr' 'libXScrnSaver' 'libXtst' 'xdg-utils') +deps_arch=('ca-certificates' 'git' 'alsa-lib' 'atk' 'at-spi2-atk' 'at-spi2-core' 'cairo' 'cups' 'dbus' 'expat' 'libgbm' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'libsecret' 'systemd-libs' 'libx11' 'libxcb' 'libxcomposite' 'libxdamage' 'libxext' 'libxfixes' 'libxkbcommon' 'libxrandr' 'libxss' 'libxtst' 'xdg-utils') +deps_opensuse=('ca-certificates' 'git' 'alsa' 'libatk-1_0-0' 'libatk-bridge-2_0-0' 'libatspi0' 'libcairo2' 'cups-libs' 'libdbus-1-3' 'libexpat1' 'libgbm1' 'glib2' 'gtk3' 'mozilla-nspr' 'mozilla-nss' 'pango' 'libsecret-1-0' 'libudev1' 'libX11-6' 'libxcb1' 'libXcomposite1' 'libXdamage1' 'libXext6' 'libXfixes3' 'libxkbcommon0' 'libXrandr2' 'libXss1' 'libXtst6' 'xdg-utils') +deps_altlinux=('ca-certificates' 'git' 'libalsa' 'at-spi2-atk' 'libat-spi2-core' 'libatk' 'libcairo' 'libcups' 'libdbus' 'libexpat' 'libgbm' 'glib2' 'libgtk+3' 'libnspr' 'libnss' 'libpango' 'libsecret' 'libudev0' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' 'libxkbcommon' 'libXrandr' 'libXScrnSaver' 'libXtst' 'xdg-utils') + +build_deps=('binutils') + +sources=( + "https://github.com/Cheviiot/Nivora/releases/download/github-desktop-${version}-linux/github-desktop-${version}-linux-x64.tar.gz?~archive=false&~name=github-desktop.tar.gz" + "https://raw.githubusercontent.com/desktop/desktop/release-${version}/LICENSE?~archive=false&~name=LICENSE" + "https://raw.githubusercontent.com/desktop/desktop/release-${version}/app/static/logos/prod/icon-logo.icon/Assets/1%20%E2%80%93%20Layer%203.svg?~archive=false&~name=github-desktop.svg" +) + +sources_arm64=( + "https://github.com/Cheviiot/Nivora/releases/download/github-desktop-${version}-linux/github-desktop-${version}-linux-arm64.tar.gz?~archive=false&~name=github-desktop.tar.gz" + "https://raw.githubusercontent.com/desktop/desktop/release-${version}/LICENSE?~archive=false&~name=LICENSE" + "https://raw.githubusercontent.com/desktop/desktop/release-${version}/app/static/logos/prod/icon-logo.icon/Assets/1%20%E2%80%93%20Layer%203.svg?~archive=false&~name=github-desktop.svg" +) + +checksums=( + 'sha256:78d55b126a200c3585c7d0e3a32b95fe5d0d19e71921169c3e376af520d9a179' + 'sha256:891d678cd6aa67c0712f663b5fee690f24d11d360795300814f7bf2eb91ba530' + 'sha256:182efd5af36dccaa389086695e38140ff42af0befd2caaa6661e4cc0ffe614b7' +) + +checksums_arm64=( + 'sha256:b1934f1d8d621ee879ef6b7d7dabe9704134c4dc861813b776f42deedce5a038' + 'sha256:891d678cd6aa67c0712f663b5fee690f24d11d360795300814f7bf2eb91ba530' + 'sha256:182efd5af36dccaa389086695e38140ff42af0befd2caaa6661e4cc0ffe614b7' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + tar -xzf github-desktop.tar.gz + [[ -x "${srcdir}/github-desktop/desktop" ]] + + install -dm755 "${pkgdir}/opt/github-desktop" + cp -a "${srcdir}/github-desktop/." "${pkgdir}/opt/github-desktop/" + chmod 0755 "${pkgdir}/opt/github-desktop/desktop" + if [[ -f "${pkgdir}/opt/github-desktop/chrome-sandbox" ]]; then + chmod 0755 "${pkgdir}/opt/github-desktop/chrome-sandbox" + fi + + install -dm755 "${pkgdir}/usr/bin" + ln -sfn /opt/github-desktop/desktop "${pkgdir}/usr/bin/github-desktop" + + install -Dm644 github-desktop.svg \ + "${pkgdir}/usr/share/icons/hicolor/scalable/apps/github-desktop.svg" + install -Dm644 /dev/stdin \ + "${pkgdir}/usr/share/applications/github-desktop.desktop" <<'EOF' +[Desktop Entry] +Name=GitHub Desktop +Comment=Simple collaboration from your desktop +Comment[ru]=Графический клиент для работы с GitHub +Exec=/usr/bin/github-desktop --protocol-launcher %U +Terminal=false +Type=Application +Icon=github-desktop +Categories=Development;RevisionControl; +MimeType=x-scheme-handler/x-github-desktop-auth;x-scheme-handler/x-github-client;x-scheme-handler/github-mac; +StartupWMClass=desktop +EOF + + install-license LICENSE "github-desktop/LICENSE" + install -Dm644 "${srcdir}/github-desktop/LICENSE" \ + "${pkgdir}/usr/share/licenses/github-desktop/LICENSE.electron.txt" + install -Dm644 "${srcdir}/github-desktop/LICENSES.chromium.html" \ + "${pkgdir}/usr/share/licenses/github-desktop/LICENSES.chromium.html" +} + +files() { + files-find "/opt/github-desktop/**/*" + files-find-binary + files-find-desktop + files-find-license + files-find "/usr/share/icons/hicolor/*/apps/github-desktop.svg" +} diff --git a/github-desktop/postinstall.sh b/github-desktop/postinstall.sh new file mode 100755 index 0000000..b6f7d74 --- /dev/null +++ b/github-desktop/postinstall.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -euo pipefail + +sandbox=/opt/github-desktop/chrome-sandbox +if [[ -f "$sandbox" ]]; then + if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \ + && unshare --user true 2>/dev/null; then + chmod 0755 "$sandbox" + else + chmod 4755 "$sandbox" + fi +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -f -q /usr/share/icons/hicolor || true +fi diff --git a/github-desktop/postremove.sh b/github-desktop/postremove.sh new file mode 100755 index 0000000..c18930b --- /dev/null +++ b/github-desktop/postremove.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail + +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -f -q /usr/share/icons/hicolor || true +fi diff --git a/happ/.stapler/update-check b/happ/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/happ/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/happ/LICENSE b/happ/LICENSE new file mode 100644 index 0000000..2ee1b0d --- /dev/null +++ b/happ/LICENSE @@ -0,0 +1,12 @@ +Сведения о лицензии пакета Happ + +Лицензия: Custom. +Официальный сайт: https://happ.su/ +Условия лицензии: https://happ.su/license + +Нюансы пакета: +- Пакет перепаковывает официальный deb-артефакт Happ Desktop для Linux. +- Happ является проприетарным приложением FlyFrog LLC. +- В составе приложения могут использоваться проприетарные компоненты и сетевые функции, связанные с xray-core/sing-box-сценариями. +- Использование клиента, подписок и подключений регулируется условиями Happ и провайдера сервиса. +- Пакет помечен как несвободный через `nonfree=1`. diff --git a/happ/Staplerfile b/happ/Staplerfile new file mode 100644 index 0000000..8b80f61 --- /dev/null +++ b/happ/Staplerfile @@ -0,0 +1,336 @@ +name='happ' +version=3.3.6 +release=4 +summary='User-friendly GUI proxy client for xray-core' +summary_ru='Удобный GUI-прокси-клиент для xray-core' +group='Networking/Other' +desc='Happ is a GUI client for xray-core with TUN/VPN mode via sing-box, anti-censorship tools, QR code import and subscription management.' +desc_ru='Happ — GUI-клиент для xray-core с режимом TUN/VPN через sing-box, средствами обхода блокировок, импортом QR-кодов и управлением подписками.' +homepage='https://happ.su/' +maintainer='FlyFrog LLC ' + +architectures=('amd64' 'arm64') + +license=('Custom') +nonfree=1 +nonfree_msg='Happ является проприетарным приложением FlyFrog LLC. Использование клиента, подписок и сетевых функций регулируется условиями Happ.' +nonfree_url='https://happ.su/license' + +provides=() +replaces=('happ') +conflicts=() + +# auto_req отключен — Qt-приложение поставляется с bundled библиотеками +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('libc6' 'libssl3 | libssl3t64') +deps_ubuntu=('libc6' 'libssl3 | libssl3t64') +deps_fedora=('glibc' 'openssl-libs') +deps_arch=('glibc' 'openssl') +deps_opensuse=('glibc' 'libopenssl1_1') +deps_altlinux=('glibc' 'libssl3') + +build_deps=('binutils') +build_deps_debian=('binutils' 'python3') +build_deps_ubuntu=("${build_deps_debian[@]}") +build_deps_fedora=('binutils' 'python3') +build_deps_arch=('binutils' 'python') +build_deps_opensuse=('binutils' 'python3') +build_deps_altlinux=('binutils' 'python3') + +sources=( + "https://github.com/Happ-proxy/happ-desktop/releases/download/${version}/Happ.linux.x64.deb?~archive=false&~name=happ.deb" + 'local:///patch-tray-icons.py' + 'local:///happ-launcher' + 'local:///tray-idle.svg' + 'local:///tray-connected.svg' + 'local:///LICENSE' +) + +sources_arm64=( + "https://github.com/Happ-proxy/happ-desktop/releases/download/${version}/Happ.linux.arm64.deb?~archive=false&~name=happ.deb" + 'local:///patch-tray-icons.py' + 'local:///happ-launcher' + 'local:///tray-idle.svg' + 'local:///tray-connected.svg' + 'local:///LICENSE' +) + +checksums=( + 'a7dac51277387bfe1049b1ad40f40f2e74af233a5eab020b5be1a622effc46a4' + '5b0106dda286b039d6bdce45208f44ea3eb7a2deb4e00455190ce7a07177dde0' + 'fa4d741a4e2df82b0a19291e2c7bad4a8753a1e59237b2bfe5fef7de8c22efd2' + 'af47a069497a8063b9028549dc6db111776589818c6165c7a46cb8b25a822ead' + '7e105f63e4535676e15e4d613426d6af843376a09fd25674bbbf367778668ae5' + '5f0fbc1b0b0e8399c8a04425512168e3dd87960381e85a7cb62a41d55b054b0f' +) + +checksums_arm64=( + 'a4d3d6dcab1db61db23cdf0f86bce736014887262b570927befe48cb49f14aa6' + '5b0106dda286b039d6bdce45208f44ea3eb7a2deb4e00455190ce7a07177dde0' + 'fa4d741a4e2df82b0a19291e2c7bad4a8753a1e59237b2bfe5fef7de8c22efd2' + 'af47a069497a8063b9028549dc6db111776589818c6165c7a46cb8b25a822ead' + '7e105f63e4535676e15e4d613426d6af843376a09fd25674bbbf367778668ae5' + '5f0fbc1b0b0e8399c8a04425512168e3dd87960381e85a7cb62a41d55b054b0f' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/happ-extracted" + ar x happ.deb + tar -xf data.tar.* -C "${srcdir}/happ-extracted" + + cp -a "${srcdir}/happ-extracted/opt" "${pkgdir}/opt" + python3 patch-tray-icons.py \ + "${pkgdir}/opt/happ/bin/Happ" \ + tray-idle.svg \ + tray-connected.svg + + install-systemd "${srcdir}/happ-extracted/etc/systemd/system/happd.service" + + sed -i 's|^Exec=/opt/happ/bin/Happ|Exec=/usr/bin/happ|' \ + "${srcdir}/happ-extracted/usr/share/applications/Happ.desktop" + install-desktop "${srcdir}/happ-extracted/usr/share/applications/Happ.desktop" + + python3 - \ + "${srcdir}/happ-extracted/usr/share/icons/hicolor/256x256/apps/happ.png" \ + "${pkgdir}/usr/share/icons/hicolor/512x512/apps/happ.png" <<'PY' +import math +import struct +import sys +import zlib +from pathlib import Path + +PNG_SIG = b"\x89PNG\r\n\x1a\n" + + +def read_png(path): + data = Path(path).read_bytes() + if not data.startswith(PNG_SIG): + raise ValueError("not a PNG") + + pos = len(PNG_SIG) + width = height = bit_depth = color_type = None + idat = bytearray() + while pos < len(data): + length = struct.unpack(">I", data[pos:pos + 4])[0] + pos += 4 + chunk_type = data[pos:pos + 4] + pos += 4 + chunk = data[pos:pos + length] + pos += length + 4 + if chunk_type == b"IHDR": + width, height, bit_depth, color_type, _, _, _ = struct.unpack( + ">IIBBBBB", chunk + ) + elif chunk_type == b"IDAT": + idat.extend(chunk) + elif chunk_type == b"IEND": + break + + if bit_depth != 8 or color_type not in (2, 6): + raise ValueError("unsupported PNG format") + + channels = 4 if color_type == 6 else 3 + raw = zlib.decompress(bytes(idat)) + stride = width * channels + rows = [] + prev = [0] * stride + index = 0 + + for _ in range(height): + filter_type = raw[index] + index += 1 + encoded = list(raw[index:index + stride]) + index += stride + row = [0] * stride + + for x, value in enumerate(encoded): + left = row[x - channels] if x >= channels else 0 + up = prev[x] + upper_left = prev[x - channels] if x >= channels else 0 + + if filter_type == 0: + decoded = value + elif filter_type == 1: + decoded = (value + left) & 255 + elif filter_type == 2: + decoded = (value + up) & 255 + elif filter_type == 3: + decoded = (value + ((left + up) // 2)) & 255 + elif filter_type == 4: + predictor = left + up - upper_left + distances = ( + abs(predictor - left), + abs(predictor - up), + abs(predictor - upper_left), + ) + decoded = (value + (left, up, upper_left)[distances.index(min(distances))]) & 255 + else: + raise ValueError("unsupported PNG filter") + + row[x] = decoded + + prev = row + rows.append(row) + + pixels = [] + for row in rows: + out_row = [] + for x in range(width): + offset = x * channels + if channels == 4: + out_row.append(tuple(row[offset:offset + 4])) + else: + out_row.append((row[offset], row[offset + 1], row[offset + 2], 255)) + pixels.append(out_row) + + return width, height, pixels + + +def write_png(path, width, height, pixels): + def chunk(chunk_type, payload): + crc = zlib.crc32(chunk_type + payload) & 0xFFFFFFFF + return struct.pack(">I", len(payload)) + chunk_type + payload + struct.pack(">I", crc) + + raw = bytearray() + for y in range(height): + raw.append(0) + for x in range(width): + raw.extend(bytes(pixels[y][x])) + + ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) + Path(path).parent.mkdir(parents=True, exist_ok=True) + Path(path).write_bytes( + PNG_SIG + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + + chunk(b"IEND", b"") + ) + + +def rounded_rect_coverage(px, py, size, margin, radius, samples=4): + covered = 0 + left = top = margin + right = bottom = size - margin + + for sy in range(samples): + for sx in range(samples): + x = px + (sx + 0.5) / samples + y = py + (sy + 0.5) / samples + if x < left or x > right or y < top or y > bottom: + continue + cx = min(max(x, left + radius), right - radius) + cy = min(max(y, top + radius), bottom - radius) + if (x - cx) ** 2 + (y - cy) ** 2 <= radius ** 2: + covered += 1 + + return covered / (samples * samples) + + +def sample_alpha(alpha, x, y): + height = len(alpha) + width = len(alpha[0]) + if x < 0 or y < 0 or x > width - 1 or y > height - 1: + return 0.0 + + x0 = int(math.floor(x)) + y0 = int(math.floor(y)) + x1 = min(x0 + 1, width - 1) + y1 = min(y0 + 1, height - 1) + dx = x - x0 + dy = y - y0 + + top = alpha[y0][x0] * (1 - dx) + alpha[y0][x1] * dx + bottom = alpha[y1][x0] * (1 - dx) + alpha[y1][x1] * dx + return top * (1 - dy) + bottom * dy + + +src_path, dest_path = sys.argv[1], sys.argv[2] +src_width, src_height, source = read_png(src_path) +background = 43 + +logo_alpha = [] +for y in range(src_height): + row = [] + for x in range(src_width): + r, g, b, a = source[y][x] + luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b + row.append(max(0.0, min(1.0, (luminance - background) / (255 - background))) * (a / 255.0)) + logo_alpha.append(row) + +xs = [x for y in range(src_height) for x in range(src_width) if logo_alpha[y][x] > 0.04] +ys = [y for y in range(src_height) for x in range(src_width) if logo_alpha[y][x] > 0.04] +left, right = min(xs), max(xs) +top, bottom = min(ys), max(ys) +crop_width = right - left + 1 +crop_height = bottom - top + 1 +cropped = [ + [logo_alpha[y][x] for x in range(left, right + 1)] + for y in range(top, bottom + 1) +] + +size = 512 +margin = 38 +radius = 116 +logo_height = 292 +logo_width = round(crop_width * logo_height / crop_height) +logo_x = (size - logo_width) // 2 +logo_y = (size - logo_height) // 2 + 6 + +pixels = [[(0, 0, 0, 0) for _ in range(size)] for _ in range(size)] +for y in range(size): + for x in range(size): + coverage = rounded_rect_coverage(x, y, size, margin, radius) + if coverage: + pixels[y][x] = (43, 43, 43, round(255 * coverage)) + +for y in range(logo_height): + source_y = (y + 0.5) * crop_height / logo_height - 0.5 + target_y = logo_y + y + if target_y < 0 or target_y >= size: + continue + + for x in range(logo_width): + source_x = (x + 0.5) * crop_width / logo_width - 0.5 + target_x = logo_x + x + if target_x < 0 or target_x >= size: + continue + + alpha = sample_alpha(cropped, source_x, source_y) + if alpha <= 0: + continue + + dr, dg, db, da = pixels[target_y][target_x] + da_float = da / 255.0 + out_alpha = alpha + da_float * (1 - alpha) + out_rgb = round((255 * alpha + dr * da_float * (1 - alpha)) / out_alpha) + pixels[target_y][target_x] = (out_rgb, out_rgb, out_rgb, round(out_alpha * 255)) + +write_png(dest_path, size, size, pixels) +PY + + install -Dm644 "${srcdir}/happ-extracted/usr/share/mime/packages/happ-mime.xml" \ + "${pkgdir}/usr/share/mime/packages/happ-mime.xml" + + install -Dm755 "${srcdir}/happ-launcher" "${pkgdir}/usr/bin/happ" + install-license "${srcdir}/LICENSE" "happ/LICENSE.nivora" +} + +files() { + files-find "/opt/happ/**/*" + files-find-binary + files-find-desktop + files-find-systemd + files-find-license + files-find \ + "/usr/share/icons/hicolor/512x512/apps/happ.png" \ + "/usr/share/mime/packages/happ-mime.xml" +} diff --git a/happ/happ-launcher b/happ/happ-launcher new file mode 100755 index 0000000..a8e337d --- /dev/null +++ b/happ/happ-launcher @@ -0,0 +1,9 @@ +#!/bin/sh + +# Happ's bundled Qt falls back to the generic GNOME platform theme, which +# reports Qt::ColorScheme::Unknown. The bundled portal plugin follows the +# desktop color-scheme setting and emits updates when it changes. +QT_QPA_PLATFORMTHEME="${QT_QPA_PLATFORMTHEME:-xdgdesktopportal}" +export QT_QPA_PLATFORMTHEME + +exec /opt/happ/bin/Happ "$@" diff --git a/happ/patch-tray-icons.py b/happ/patch-tray-icons.py new file mode 100755 index 0000000..369085e --- /dev/null +++ b/happ/patch-tray-icons.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import struct +import sys +from pathlib import Path + + +UPSTREAM_RESOURCES = { + "idle": ( + "c0939b3a81a59983f0db07cdfa08eae90d6ddc211b7b790c425beb72628355ea", + b' tuple[int, int]: + expected_hash, marker = UPSTREAM_RESOURCES[kind] + matches: list[tuple[int, int]] = [] + offset = 0 + + while True: + start = binary.find(marker, offset) + if start < 0: + break + offset = start + 1 + if start < 4: + continue + + length = struct.unpack_from(">I", binary, start - 4)[0] + if length <= 0 or start + length > len(binary): + continue + payload = bytes(binary[start:start + length]) + if ( + hashlib.sha256(payload).hexdigest() == expected_hash + and (expected_start is None or start == expected_start) + ): + matches.append((start, length)) + + if len(matches) != 1: + raise RuntimeError( + f"expected exactly one upstream {kind} tray resource, found {len(matches)}" + ) + + start, capacity = matches[0] + if len(replacement) > capacity: + raise RuntimeError( + f"{kind} tray replacement is too large: {len(replacement)} > {capacity}" + ) + + struct.pack_into(">I", binary, start - 4, len(replacement)) + binary[start:start + len(replacement)] = replacement + binary[start + len(replacement):start + capacity] = b"\0" * ( + capacity - len(replacement) + ) + return start, capacity + + +def main() -> int: + if len(sys.argv) != 4: + print( + "usage: patch-tray-icons.py ", + file=sys.stderr, + ) + return 2 + + binary_path = Path(sys.argv[1]) + binary = bytearray(binary_path.read_bytes()) + connected_start, connected_capacity = replace_resource( + binary, "connected", Path(sys.argv[3]).read_bytes() + ) + replace_resource( + binary, + "idle", + Path(sys.argv[2]).read_bytes(), + connected_start + connected_capacity + 4, + ) + binary_path.write_bytes(binary) + print("Patched Happ tray icons") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/happ/postinstall.sh b/happ/postinstall.sh new file mode 100755 index 0000000..275019a --- /dev/null +++ b/happ/postinstall.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +# Обновляем MIME-базу данных +if command -v update-mime-database &>/dev/null; then + optional_refresh "MIME-базу" update-mime-database /usr/share/mime +fi + +# Обновляем desktop базу данных +if command -v update-desktop-database &>/dev/null; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi + +# Обновляем кэш иконок +if command -v gtk-update-icon-cache &>/dev/null; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then + systemctl daemon-reload + systemctl enable --now happd.service +fi diff --git a/happ/postremove.sh b/happ/postremove.sh new file mode 100755 index 0000000..1870873 --- /dev/null +++ b/happ/postremove.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +unit=/usr/lib/systemd/system/happd.service +if [[ ! -e "$unit" ]] && command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then + if systemctl is-active --quiet happd.service; then + systemctl stop happd.service + fi + if systemctl is-enabled --quiet happd.service; then + systemctl disable happd.service + fi + systemctl daemon-reload +fi + +# Обновляем MIME-базу данных +if command -v update-mime-database &>/dev/null; then + optional_refresh "MIME-базу" update-mime-database /usr/share/mime +fi + +# Обновляем desktop базу данных +if command -v update-desktop-database &>/dev/null; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi + +# Обновляем кэш иконок +if command -v gtk-update-icon-cache &>/dev/null; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi diff --git a/happ/stapler-repo.toml b/happ/stapler-repo.toml new file mode 100644 index 0000000..d35f9bf --- /dev/null +++ b/happ/stapler-repo.toml @@ -0,0 +1 @@ +[inherit] diff --git a/happ/tray-connected.svg b/happ/tray-connected.svg new file mode 100644 index 0000000..bb82951 --- /dev/null +++ b/happ/tray-connected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/happ/tray-idle.svg b/happ/tray-idle.svg new file mode 100644 index 0000000..bde9263 --- /dev/null +++ b/happ/tray-idle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/netbird/.stapler/update-check b/netbird/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/netbird/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/netbird/LICENSE b/netbird/LICENSE new file mode 100644 index 0000000..3370548 --- /dev/null +++ b/netbird/LICENSE @@ -0,0 +1,16 @@ +Сведения о лицензии пакета NetBird + +Лицензии: BSD-3-Clause и AGPL-3.0-only. +Исходный проект: https://github.com/netbirdio/netbird +Официальный сайт: https://netbird.io/ + +Нюансы пакета: +- Пакет перепаковывает официальный deb-артефакт NetBird CLI/daemon для Linux. +- Основные части репозитория NetBird распространяются под BSD-3-Clause. +- Отдельные серверные компоненты исходного репозитория, включая management, signal, relay и combined, лицензированы под AGPL-3.0. +- Данный пакет устанавливает клиентский бинарник и systemd-сервис; серверная инфраструктура NetBird в пакет не входит. +- Подключение к облачному или собственному management-серверу регулируется условиями соответствующего сервиса. + +Официальные тексты лицензий: +- https://opensource.org/license/bsd-3-clause +- https://www.gnu.org/licenses/agpl-3.0.txt diff --git a/netbird/Staplerfile b/netbird/Staplerfile new file mode 100644 index 0000000..1a2bfb2 --- /dev/null +++ b/netbird/Staplerfile @@ -0,0 +1,90 @@ +name='netbird' +version=0.75.0 +release=1 +summary='WireGuard-based mesh VPN client' +summary_ru='Mesh VPN-клиент на базе WireGuard' +group='Networking/Other' +desc='NetBird is a WireGuard-based mesh VPN client with SSO, MFA and policy-based access control for connecting devices and private networks.' +desc_ru='NetBird — mesh VPN-клиент на базе WireGuard с SSO, MFA и политиками доступа для соединения устройств и приватных сетей.' +homepage='https://netbird.io/' +maintainer='NetBird GmbH ' + +architectures=('amd64' 'arm64') + +license=('BSD-3-Clause' 'AGPL-3.0-only') + +provides=() +replaces=('netbird') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('ca-certificates' 'iproute2' 'iptables') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('ca-certificates' 'iproute' 'iptables') +deps_arch=('ca-certificates' 'iproute2' 'iptables') +deps_opensuse=('ca-certificates' 'iproute2' 'iptables') +deps_altlinux=('ca-certificates' 'iproute2' 'iptables') + +build_deps=('binutils') + +sources=( + "https://github.com/netbirdio/netbird/releases/download/v${version}/netbird_${version}_linux_amd64.deb?~archive=false&~name=netbird.deb" + "local:///netbird.png" + "local:///netbird.service" + "local:///netbird-purge-data" + 'local:///LICENSE' +) + +sources_arm64=( + "https://github.com/netbirdio/netbird/releases/download/v${version}/netbird_${version}_linux_arm64.deb?~archive=false&~name=netbird.deb" + "local:///netbird.png" + "local:///netbird.service" + "local:///netbird-purge-data" + 'local:///LICENSE' +) + +checksums=( + 'sha256:b65f65d7f32831639ebdd44c241e7a2310287723b64dbc03c93292853042ee6f' + 'sha256:71b133636e91eb67f221f64592f64fa04a3b2337f4d616be36492efae0cff643' + 'sha256:abd8064281462a4ce12c2fdf2e9436d9ae6932ff3b366668a175f2a556169012' + 'sha256:bb5c44ab330db0d63a82b582f2c508681a6922ea65f3bf2becaeed87693215b1' + 'sha256:11555e8308e949c3965a681f48b83f7f2488cff03b09772d8af4a20d376464e7' +) + +checksums_arm64=( + 'sha256:9e57e854da0bbaf392cc6f99e37db75809e21c3effde0fca9f3bc9dc90245c02' + 'sha256:71b133636e91eb67f221f64592f64fa04a3b2337f4d616be36492efae0cff643' + 'sha256:abd8064281462a4ce12c2fdf2e9436d9ae6932ff3b366668a175f2a556169012' + 'sha256:bb5c44ab330db0d63a82b582f2c508681a6922ea65f3bf2becaeed87693215b1' + 'sha256:11555e8308e949c3965a681f48b83f7f2488cff03b09772d8af4a20d376464e7' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x netbird.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + install-binary "${srcdir}/extracted/usr/bin/netbird" + install-binary "${srcdir}/netbird-purge-data" + install-systemd "${srcdir}/netbird.service" + install-license "${srcdir}/LICENSE" "netbird/LICENSE.nivora" + install -Dm644 "${srcdir}/netbird.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/netbird.png" +} + +files() { + files-find-binary + files-find-systemd + files-find-license + files-find "/usr/share/icons/hicolor/256x256/apps/netbird.png" +} diff --git a/netbird/netbird-purge-data b/netbird/netbird-purge-data new file mode 100755 index 0000000..b054a10 --- /dev/null +++ b/netbird/netbird-purge-data @@ -0,0 +1,29 @@ +#!/bin/bash + +set -euo pipefail + +if [[ "${1:-}" != "--yes" || "$#" -ne 1 ]]; then + echo "Использование: sudo netbird-purge-data --yes" >&2 + echo "Команда отменит регистрацию узла NetBird и безвозвратно удалит локальное состояние." >&2 + exit 2 +fi + +if [[ "${EUID}" -ne 0 ]]; then + echo "Ошибка: очистку данных NetBird нужно запускать от root." >&2 + exit 1 +fi + +if command -v netbird >/dev/null 2>&1; then + if ! netbird deregister; then + echo "Предупреждение: NetBird не подтвердил deregister; локальные данные всё равно будут удалены." >&2 + fi +fi + +if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then + systemctl stop netbird.service +fi + +rm -rf -- /var/lib/netbird /var/cache/netbird /var/log/netbird +rm -f -- /run/netbird.sock /var/run/netbird.sock + +echo "Локальные данные NetBird удалены." diff --git a/netbird/netbird.png b/netbird/netbird.png new file mode 100644 index 0000000..a92e9ed Binary files /dev/null and b/netbird/netbird.png differ diff --git a/netbird/netbird.service b/netbird/netbird.service new file mode 100644 index 0000000..7dd5ac9 --- /dev/null +++ b/netbird/netbird.service @@ -0,0 +1,19 @@ +[Unit] +Description=NetBird mesh network client +Documentation=https://docs.netbird.io/ +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +EnvironmentFile=-/etc/sysconfig/netbird +ExecStart=/usr/bin/netbird service run --log-level info --daemon-addr unix:///run/netbird.sock --log-file /var/log/netbird/client.log +Restart=on-failure +RestartSec=5s +StateDirectory=netbird +StateDirectoryMode=0700 +LogsDirectory=netbird +LogsDirectoryMode=0750 + +[Install] +WantedBy=multi-user.target diff --git a/netbird/postinstall.sh b/netbird/postinstall.sh new file mode 100755 index 0000000..ee204c0 --- /dev/null +++ b/netbird/postinstall.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +legacy_unit=/etc/systemd/system/netbird.service +if [[ -f "$legacy_unit" ]] && grep -Fq 'ExecStart=/usr/bin/netbird "service" "run"' "$legacy_unit"; then + rm -f -- "$legacy_unit" +fi + +if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then + systemctl daemon-reload + systemctl enable --now netbird.service +fi diff --git a/netbird/postremove.sh b/netbird/postremove.sh new file mode 100755 index 0000000..72add51 --- /dev/null +++ b/netbird/postremove.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euo pipefail + +unit=/usr/lib/systemd/system/netbird.service + +if [[ ! -e "$unit" ]] && command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then + if systemctl is-active --quiet netbird.service; then + systemctl stop netbird.service + fi + if systemctl is-enabled --quiet netbird.service; then + systemctl disable netbird.service + fi + systemctl daemon-reload +fi diff --git a/netbird/stapler-repo.toml b/netbird/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/netbird/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/nivora-stplr/.stapler/update-check b/nivora-stplr/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/nivora-stplr/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/nivora-stplr/LICENSE b/nivora-stplr/LICENSE new file mode 100644 index 0000000..dc1024b --- /dev/null +++ b/nivora-stplr/LICENSE @@ -0,0 +1,16 @@ +Сведения о лицензии пакета Nivora Stapler Helper + +Лицензия: MIT. +Исходный проект: https://github.com/Cheviiot/Nivora + +Нюансы пакета: +- Пакет устанавливает CLI-helper для Stapler с командами `sl`, `sli`, `slr`, `slu`, `slf`, `slref`, `sls`, `slii` и `sll`. +- Helper не заменяет Stapler: все операции выполняются через системную команду `stplr`. +- Через команду повышения привилегий запускаются только `install`, `remove`, `up`, `fix` и `refresh`. +- Команды чтения `search`, `info` и `list` выполняются без повышения привилегий. +- Префикс репозитория задается через `--repo ` или `NIVORA_STPLR_REPO`; по умолчанию короткие имена не привязаны к одному репозиторию. +- Пути вида `repo/package` не изменяются. +- Helper не исполняет произвольные shell-строки через `sudo`. + +Полный официальный текст MIT: +https://opensource.org/licenses/MIT diff --git a/nivora-stplr/Staplerfile b/nivora-stplr/Staplerfile new file mode 100644 index 0000000..e943c29 --- /dev/null +++ b/nivora-stplr/Staplerfile @@ -0,0 +1,55 @@ +name='nivora-stplr' +version='0.3.0' +release=1 +summary='Short command helper for Stapler' +summary_ru='Короткие команды для Stapler' +desc='Nivora Stapler Helper provides short commands for common Stapler operations across configured repositories and elevates only operations that modify packages or caches.' +desc_ru='Nivora Stapler Helper добавляет короткие команды для типовых операций Stapler и повышает привилегии только для изменения пакетов и кэша.' +group='System/Configuration/Packaging' +homepage='https://github.com/Cheviiot/Nivora' +maintainer='Cheviiot ' + +architectures=('all') +license=('MIT') + +provides=() +replaces=('nivora-stplr') +conflicts=() + +auto_reqprov_method='dirty' +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('bash' 'sudo') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('bash' 'sudo') +deps_arch=('bash' 'sudo') +deps_altlinux=('bash' 'sudo') +deps_opensuse=('bash' 'sudo') + +sources=( + 'local:///nivora-stplr' + 'local:///LICENSE' +) + +checksums=( + 'sha256:7b0c2a69aed30bc39ac5546aabd0cb82d45b857a52408b4326ad1fd667684ecb' + 'sha256:c5674e6873211a8ea0f08ee3a950f809c9145c60750746dafff9aee7e1c21cf1' +) + +package() { + install -Dm755 "${srcdir}/nivora-stplr" "${pkgdir}/usr/bin/sl" + + shortcut='' + for shortcut in sli slr slu slf slref sls slii sll; do + ln -sfn sl "${pkgdir}/usr/bin/${shortcut}" + done + + install-license "${srcdir}/LICENSE" 'nivora-stplr/LICENSE.nivora' +} + +files() { + files-find-binary + files-find-license +} diff --git a/nivora-stplr/nivora-stplr b/nivora-stplr/nivora-stplr new file mode 100755 index 0000000..917ed54 --- /dev/null +++ b/nivora-stplr/nivora-stplr @@ -0,0 +1,239 @@ +#!/bin/bash +set -euo pipefail + +repo_prefix="${NIVORA_STPLR_REPO:-}" + +usage() { + cat <<'EOF' +Nivora Stapler Helper + +Usage: + sl [args...] + sli [stplr args...] sudo stplr install + slr [stplr args...] sudo stplr remove + slu [stplr args...] sudo stplr up + slf [stplr args...] sudo stplr fix + slref [stplr args...] sudo stplr refresh + sls [stplr args...] stplr search + slii [stplr args...] stplr info + sll [stplr args...] stplr list + +Commands: + install, in, i install package with sudo + remove, rm, r remove package with sudo + up, upgrade, u upgrade installed packages with sudo + fix, f run stplr fix with sudo + refresh, ref refresh repositories with sudo + search, s search packages + info, show show package info + list, ls list repository packages + help show this help + +Options: + --repo prefix short package names with repository, e.g. sli --repo nivora parsec + +Environment: + NIVORA_STPLR_REPO optional repository prefix for short names, default: disabled + NIVORA_STPLR_SUDO privilege command, default: sudo + NIVORA_STPLR_QUIET=1 do not print command before execution +EOF +} + +die() { + echo "nivora-stplr: $*" >&2 + exit 2 +} + +need_stplr() { + command -v stplr >/dev/null 2>&1 || die "команда stplr не найдена" +} + +sudo_cmd() { + if [[ "${EUID}" -eq 0 ]]; then + return 0 + fi + + local command_name="${NIVORA_STPLR_SUDO:-sudo}" + command -v "${command_name}" >/dev/null 2>&1 || die "команда ${command_name} не найдена" + printf '%s\n' "${command_name}" +} + +is_package_token() { + local value="$1" + + [[ -n "${value}" ]] || return 1 + [[ "${value}" == -* ]] && return 1 + [[ "${value}" == */* ]] && return 1 + [[ "${value}" == ./* || "${value}" == ../* || "${value}" == /* ]] && return 1 + [[ "${value}" == *.rpm || "${value}" == *.deb || "${value}" == *.pkg.tar.* ]] && return 1 + return 0 +} + +normalize_package() { + local value="$1" + + if [[ -n "${repo_prefix}" ]] && is_package_token "${value}"; then + printf '%s/%s\n' "${repo_prefix}" "${value}" + else + printf '%s\n' "${value}" + fi +} + +normalize_package_args() { + local arg + + for arg in "$@"; do + normalize_package "${arg}" + done +} + +extract_repo_option() { + local -n output_ref="$1" + shift + + output_ref=() + while [[ "$#" -gt 0 ]]; do + case "$1" in + --repo) + [[ "$#" -ge 2 ]] || die "для --repo нужно имя репозитория" + repo_prefix="$2" + shift 2 + ;; + --repo=*) + repo_prefix="${1#--repo=}" + shift + ;; + *) + output_ref+=("$1") + shift + ;; + esac + done +} + +print_and_exec() { + local -a command=("$@") + + if [[ "${NIVORA_STPLR_QUIET:-0}" != "1" ]]; then + printf '=>' + printf ' %q' "${command[@]}" + printf '\n' + fi + + exec "${command[@]}" +} + +run_stplr() { + local mode="$1" + shift + + need_stplr + + local -a command=() + if [[ "${mode}" == "sudo" ]]; then + local privilege + privilege="$(sudo_cmd)" + if [[ -n "${privilege}" ]]; then + command+=("${privilege}") + fi + fi + + command+=(stplr "$@") + print_and_exec "${command[@]}" +} + +require_args() { + local command_name="$1" + local count="$2" + + if [[ "${count}" -eq 0 ]]; then + die "для команды ${command_name} нужен аргумент" + fi +} + +dispatch() { + local command_name="$1" + shift + + case "${command_name}" in + install | in | i) + require_args "${command_name}" "$#" + local -a args=() + extract_repo_option args "$@" + require_args "${command_name}" "${#args[@]}" + mapfile -t normalized < <(normalize_package_args "${args[@]}") + run_stplr sudo install "${normalized[@]}" + ;; + remove | rm | r | erase | delete | del) + require_args "${command_name}" "$#" + local -a args=() + extract_repo_option args "$@" + require_args "${command_name}" "${#args[@]}" + mapfile -t normalized < <(normalize_package_args "${args[@]}") + run_stplr sudo remove "${normalized[@]}" + ;; + up | upgrade | u) + run_stplr sudo up "$@" + ;; + fix | f) + run_stplr sudo fix "$@" + ;; + refresh | ref) + run_stplr sudo refresh "$@" + ;; + search | s) + require_args "${command_name}" "$#" + run_stplr user search "$@" + ;; + info | show) + require_args "${command_name}" "$#" + local -a args=() + extract_repo_option args "$@" + require_args "${command_name}" "${#args[@]}" + mapfile -t normalized < <(normalize_package_args "${args[@]}") + run_stplr user info "${normalized[@]}" + ;; + list | ls | l) + run_stplr user list "$@" + ;; + version | support) + run_stplr user "${command_name}" "$@" + ;; + help | -h | --help | "") + usage + ;; + *) + die "неизвестная команда: ${command_name}. Запустите: sl help" + ;; + esac +} + +main() { + local invoked + invoked="$(basename "$0")" + + case "${invoked}" in + sli) dispatch install "$@" ;; + slr) dispatch remove "$@" ;; + slu) dispatch up "$@" ;; + slf) dispatch fix "$@" ;; + slref) dispatch refresh "$@" ;; + sls) dispatch search "$@" ;; + slii) dispatch info "$@" ;; + sll) dispatch list "$@" ;; + sl) + if [[ "$#" -eq 0 ]]; then + usage + return 0 + fi + local command_name="$1" + shift + dispatch "${command_name}" "$@" + ;; + *) + die "неизвестное имя запуска: ${invoked}" + ;; + esac +} + +main "$@" diff --git a/nivora-stplr/stapler-repo.toml b/nivora-stplr/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/nivora-stplr/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/opencode/.stapler/update-check b/opencode/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/opencode/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/opencode/LICENSE b/opencode/LICENSE new file mode 100644 index 0000000..bc5bd5b --- /dev/null +++ b/opencode/LICENSE @@ -0,0 +1,19 @@ +Сведения о лицензии пакета OpenCode + +Лицензия: MIT. +Исходный проект: https://github.com/anomalyco/opencode +Официальная страница загрузки: https://opencode.ai/ru/download + +Нюансы пакета: +- Пакет перепаковывает официальный Linux deb-артефакт OpenCode Desktop из релизов проекта. +- В пакет входит десктопное Electron-приложение OpenCode; терминальная установка через `curl | bash`, npm, bun или Homebrew не используется. +- Код OpenCode распространяется на условиях лицензии MIT. +- Лицензия MIT разрешает использование, копирование, изменение и распространение при сохранении уведомления об авторских правах и текста лицензии. +- Для работы с внешними AI-провайдерами могут потребоваться отдельные учетные записи, ключи API или подписки. Эти сервисы и их условия использования не входят в пакет. +- Данные пользователя, настройки, авторизация и рабочие каталоги создаются приложением после запуска и не поставляются внутри пакета. +- Пакет добавляет alias `opencode-desktop`, чтобы запуск desktop-приложения не конфликтовал по смыслу с терминальным OpenCode CLI. +- Alias `open-code` сохранен для совместимости с прежним upstream-именем Linux-пакета. +- Код и ресурсы приложения устанавливаются без изменений; alias `opencode-desktop` напрямую запускает upstream-бинарник. + +Полный официальный текст лицензии MIT: +https://opensource.org/license/mit diff --git a/opencode/Staplerfile b/opencode/Staplerfile new file mode 100644 index 0000000..d3aecec --- /dev/null +++ b/opencode/Staplerfile @@ -0,0 +1,94 @@ +name='opencode' +version=1.18.4 +release=1 +summary='Open source AI coding agent desktop app' +summary_ru='Десктопное приложение AI-агента OpenCode' +group='Development/Tools' +desc='OpenCode is an open source AI coding agent for terminal, IDE and desktop workflows with support for multiple model providers.' +desc_ru='OpenCode — открытый AI-агент для работы с кодом в терминале, IDE и десктопном приложении с поддержкой разных провайдеров моделей.' +homepage='https://opencode.ai' +maintainer='Anomaly Innovations Inc. ' + +architectures=('amd64' 'arm64') + +license=('MIT') + +provides=() +replaces=('opencode') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('libgtk-3-0' 'libnotify4' 'libnss3' 'libxss1' 'libxtst6' 'xdg-utils' 'libatspi2.0-0' 'libuuid1' 'libsecret-1-0') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('gtk3' 'libnotify' 'nss' 'libXScrnSaver' 'libXtst' 'xdg-utils' 'at-spi2-core' 'libuuid' 'libsecret') +deps_arch=('gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'libutil-linux' 'libsecret') +deps_opensuse=('gtk3' 'libnotify4' 'mozilla-nss' 'libXScrnSaver1' 'libXtst6' 'xdg-utils' 'at-spi2-core' 'libuuid1' 'libsecret-1-0') +deps_altlinux=('libgtk+3' 'libnotify' 'libnss' 'libXScrnSaver' 'libXtst' 'xdg-utils' 'libat-spi2-core' 'libuuid' 'libsecret') + +opt_deps=() +opt_deps_debian=('libappindicator3-1' 'gnome-keyring') +opt_deps_ubuntu=("${opt_deps_debian[@]}") +opt_deps_fedora=('libappindicator-gtk3' 'gnome-keyring') +opt_deps_arch=('libappindicator-gtk3' 'gnome-keyring') +opt_deps_opensuse=('libappindicator3-1' 'gnome-keyring') +opt_deps_altlinux=('libappindicator-gtk3' 'gnome-keyring') + +build_deps=('binutils') + +sources=( + "https://github.com/anomalyco/opencode/releases/download/v${version}/opencode-desktop-linux-amd64.deb?~archive=false&~name=opencode.deb" + 'local:///LICENSE' +) + +sources_arm64=( + "https://github.com/anomalyco/opencode/releases/download/v${version}/opencode-desktop-linux-arm64.deb?~archive=false&~name=opencode.deb" + 'local:///LICENSE' +) + +checksums=( + 'sha256:a67bc47a98ded10275e7a8f32fa3e16480b55996cd52232da91450432b4d6599' + 'sha256:5c888de95824a9a5752d1ee3de95e80200eb057eec4078df892a95ac34333888' +) + +checksums_arm64=( + 'sha256:09bd2be9951ba5a7a333db818de49524edc6c98fedcca20d13a4a0f1c6c5b9e1' + 'sha256:5c888de95824a9a5752d1ee3de95e80200eb057eec4078df892a95ac34333888' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x opencode.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + install -dm755 "${pkgdir}/opt" + cp -a "${srcdir}/extracted/opt/OpenCode" "${pkgdir}/opt/" + + install -dm755 "${pkgdir}/usr/share" + cp -a "${srcdir}/extracted/usr/share/applications" "${pkgdir}/usr/share/" + cp -a "${srcdir}/extracted/usr/share/icons" "${pkgdir}/usr/share/" + + install -dm755 "${pkgdir}/usr/bin" + ln -sfn /opt/OpenCode/ai.opencode.desktop "${pkgdir}/usr/bin/opencode-desktop" + + install-license "${srcdir}/LICENSE" "opencode/LICENSE.nivora" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/opt/OpenCode/**/*" \ + "/usr/share/icons/hicolor/*/apps/ai.opencode.desktop.png" +} diff --git a/opencode/postinstall.sh b/opencode/postinstall.sh new file mode 100755 index 0000000..bf39e23 --- /dev/null +++ b/opencode/postinstall.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +sandbox=/opt/OpenCode/chrome-sandbox +if [[ ! -f "$sandbox" ]]; then + echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2 + exit 1 +fi +if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \ + && unshare --user true 2>/dev/null; then + chmod 0755 "$sandbox" +else + chmod 4755 "$sandbox" +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/opencode/postremove.sh b/opencode/postremove.sh new file mode 100755 index 0000000..44d2528 --- /dev/null +++ b/opencode/postremove.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/opencode/stapler-repo.toml b/opencode/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/opencode/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/parsec/.stapler/update-check b/parsec/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/parsec/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/parsec/LICENSE b/parsec/LICENSE new file mode 100644 index 0000000..9c3c549 --- /dev/null +++ b/parsec/LICENSE @@ -0,0 +1,21 @@ +Сведения о лицензии пакета Parsec + +Лицензия: Custom. +Официальная страница загрузки: https://parsec.app/downloads +Источник Linux-пакета: https://builds.parsec.app/package/parsec-linux.deb + +Нюансы пакета: +- Пакет перепаковывает официальный deb-артефакт Parsec для Linux. +- Parsec является проприетарным приложением Unity Technologies SF. +- Использование клиента, учетной записи, удаленного доступа и сетевых функций регулируется условиями Parsec/Unity. +- Пакет устанавливает клиентский бинарник `/usr/bin/parsecd` и runtime-данные `/usr/share/parsec`; учетная запись и удаленные хосты в пакет не входят. +- Команда `/usr/bin/parsecd` запускается через wrapper Nivora, который перед стартом Parsec включает `client_immersive` в пользовательском `~/.parsec/config.json` или, если JSON поврежден, в `~/.parsec/config.txt`. Это нужно, чтобы системные сочетания клавиш, включая переключение раскладки, уходили в удаленную систему, а не меняли локальную раскладку клиента. +- Wrapper учитывает новый формат Parsec 150-89+: если `config.json` является массивом, настройка записывается в объект второго элемента, без удаления существующих параметров. Если пользователь уже включил полный immersive mode через `client_immersive=1`, wrapper сохраняет это значение. +- Для обычного запуска без URL wrapper дополнительно передает `client_immersive=2` через command-line advanced setting. Запуски с `parsec://...` deep-link не переписываются, чтобы не ломать подключение по ссылке. +- Если нужно вернуть полностью upstream-поведение запуска, используйте `PARSEC_NIVORA_IMMERSIVE=0 parsecd` или измените значение `client_immersive` вручную в настройках Parsec. +- Для совместимости на ALT пакет явно требует `libjpeg8` и `libavcodec61`. +- Пакет помечен как несвободный через `nonfree=1`. + +Справочные ссылки: +- https://parsec.app/terms +- https://unity.com/legal/privacy-policy diff --git a/parsec/Staplerfile b/parsec/Staplerfile new file mode 100644 index 0000000..1bcfdd5 --- /dev/null +++ b/parsec/Staplerfile @@ -0,0 +1,81 @@ +name='parsec' +version='150-104a' +release=3 +summary='Low-latency remote desktop and game streaming client' +summary_ru='Клиент удаленного рабочего стола и игрового стриминга с низкой задержкой' +group='Networking/Remote access' +desc='Parsec is a proprietary low-latency remote desktop and game streaming client distributed as an official Linux deb package for Ubuntu 22.04 LTS Desktop.' +desc_ru='Parsec — проприетарный клиент удаленного рабочего стола и игрового стриминга с низкой задержкой, распространяемый официальным deb-пакетом для Ubuntu 22.04 LTS Desktop.' +homepage='https://parsec.app/downloads' +maintainer='Unity Technologies SF ' + +architectures=('amd64') + +license=('Custom') +nonfree=1 +nonfree_msg='Parsec является проприетарным приложением Unity Technologies SF. Использование клиента, учетной записи, удаленного доступа и сетевых функций регулируется условиями Parsec/Unity.' +nonfree_url='https://parsec.app/terms' + +provides=() +replaces=('parsec') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('libc6' 'libgcc-s1 | libgcc1' 'libstdc++6' 'libudev1' 'libssl3 | libssl1.1' 'libx11-6' 'libxcursor1' 'libxi6' 'libsm6' 'libgl1' 'libasound2' 'libjpeg8 | libjpeg-turbo8' 'libpng16-16' 'libcurl4' 'libavcodec61 | libavcodec60 | libavcodec59 | libavcodec58 | libavcodec57' 'libxrandr2' 'libvulkan1') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('glibc' 'libgcc' 'libstdc++' 'systemd-libs' 'openssl-libs' 'libX11' 'libXcursor' 'libXi' 'libSM' 'mesa-libGL' 'alsa-lib' 'libjpeg-turbo' 'libpng' 'libcurl' 'ffmpeg-libs' 'libXrandr' 'vulkan-loader') +deps_arch=('glibc' 'gcc-libs' 'systemd-libs' 'openssl' 'libx11' 'libxcursor' 'libxi' 'libsm' 'libglvnd' 'alsa-lib' 'libjpeg-turbo' 'libpng' 'curl' 'ffmpeg' 'libxrandr' 'vulkan-icd-loader') +deps_opensuse=('glibc' 'libgcc_s1' 'libstdc++6' 'libudev1' 'libopenssl3' 'libX11-6' 'libXcursor1' 'libXi6' 'libSM6' 'Mesa-libGL1' 'libasound2' 'libjpeg8' 'libpng16-16' 'libcurl4' 'libavcodec61' 'libXrandr2' 'libvulkan1') +deps_altlinux=('glibc' 'libgcc1' 'libstdc++6' 'libudev0' 'libssl3' 'libX11' 'libXcursor' 'libXi' 'libSM' 'libGL' 'libalsa' 'libjpeg8' 'libpng16' 'libcurl' 'libavcodec61' 'libXrandr' 'libvulkan1') + +build_deps=('binutils') + +sources=( + "https://builds.parsec.app/package/parsec-linux.deb?~archive=false&~name=parsec.deb" + "local:///parsecd-wrapper" + 'local:///LICENSE' +) + +checksums=( + 'sha256:c8a4407713d72dcb1c59f082977e81e1b9e8ffd679d0666882cde4c8a0821dd2' + 'sha256:2830871d7499ac00b70197ea11f2c132382059b4ad8718eb73bdcb28ad13a113' + 'sha256:d8df7db0c861a3308e59beaabe617703e0e24a92d569044432e8b2c539f1fb6c' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x parsec.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + install -Dm755 "${srcdir}/extracted/usr/bin/parsecd" "${pkgdir}/usr/lib/parsec/parsecd" + install -Dm755 parsecd-wrapper "${pkgdir}/usr/bin/parsecd" + install -Dm644 "${srcdir}/extracted/usr/share/applications/parsecd.desktop" "${pkgdir}/usr/share/applications/parsecd.desktop" + sed -i 's/^Categories=.*/Categories=Network;RemoteAccess;/' \ + "${pkgdir}/usr/share/applications/parsecd.desktop" + install -Dm644 "${srcdir}/extracted/usr/share/icons/hicolor/256x256/apps/parsecd.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/parsecd.png" + + mkdir -p "${pkgdir}/usr/share/parsec" + cp -a "${srcdir}/extracted/usr/share/parsec/." "${pkgdir}/usr/share/parsec/" + install-license "${srcdir}/LICENSE" "parsec/LICENSE.nivora" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/usr/lib/parsec/parsecd" \ + "/usr/share/icons/hicolor/256x256/apps/parsecd.png" \ + "/usr/share/parsec/**/*" +} diff --git a/parsec/parsecd-wrapper b/parsec/parsecd-wrapper new file mode 100755 index 0000000..add7e29 --- /dev/null +++ b/parsec/parsecd-wrapper @@ -0,0 +1,109 @@ +#!/bin/bash +set -euo pipefail + +real_binary='/usr/lib/parsec/parsecd' +parsec_home="${PARSEC_CONFIG_HOME:-${HOME}/.parsec}" +config_txt="${parsec_home}/config.txt" +config_json="${parsec_home}/config.json" +json_notice='See https://parsec.app/config for documentation and example. JSON must be valid before saving or file be will be erased.' + +immersive_enabled() { + case "${PARSEC_NIVORA_IMMERSIVE:-1}" in + 0 | false | no | off) return 1 ;; + *) return 0 ;; + esac +} + +set_config_txt_immersive() { + mkdir -p "${parsec_home}" + touch "${config_txt}" + + if grep -Eq '^[[:space:]]*client_immersive[[:space:]]*=' "${config_txt}"; then + sed -i -E 's/^[[:space:]]*client_immersive[[:space:]]*=.*/client_immersive=2/' "${config_txt}" + else + printf '\nclient_immersive=2\n' >>"${config_txt}" + fi +} + +set_config_json_immersive() { + command -v python3 >/dev/null 2>&1 || return 1 + + mkdir -p "${parsec_home}" + python3 - "${config_json}" "${json_notice}" <<'PY' +import json +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +notice = sys.argv[2] +data = [notice, {}] + +if path.exists() and path.stat().st_size: + try: + with path.open(encoding="utf-8") as fh: + data = json.load(fh) + except json.JSONDecodeError: + raise SystemExit(2) + +if isinstance(data, dict): + settings = data +elif isinstance(data, list): + while len(data) < 2: + data.append({}) + if not isinstance(data[0], str): + data[0] = notice + if not isinstance(data[1], dict): + data[1] = {} + settings = data[1] +else: + data = [notice, {}] + settings = data[1] + +current = settings.get("client_immersive") +value = None +if isinstance(current, dict): + value = current.get("value") + +if value in (1, 2, "1", "2"): + immersive_value = int(value) +else: + immersive_value = 2 + settings["client_immersive"] = {"value": immersive_value} + +tmp = path.with_suffix(path.suffix + ".tmp") +with tmp.open("w", encoding="utf-8") as fh: + json.dump(data, fh, ensure_ascii=False, indent=2, sort_keys=True) + fh.write("\n") +tmp.replace(path) +print(immersive_value) +PY +} + +immersive_value=2 + +if immersive_enabled; then + if ! immersive_value="$(set_config_json_immersive)"; then + set_config_txt_immersive + immersive_value=2 + fi +else + exec "${real_binary}" "$@" +fi + +for arg in "$@"; do + if [[ "${arg}" == *client_immersive=* ]]; then + exec "${real_binary}" "$@" + fi +done + +if (($# == 0)); then + exec "${real_binary}" "client_immersive=${immersive_value}" +fi + +if [[ "$1" == *=* ]]; then + first_arg="client_immersive=${immersive_value}:${1}" + shift + exec "${real_binary}" "${first_arg}" "$@" +fi + +exec "${real_binary}" "$@" diff --git a/parsec/postinstall.sh b/parsec/postinstall.sh new file mode 100755 index 0000000..44d2528 --- /dev/null +++ b/parsec/postinstall.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/parsec/postremove.sh b/parsec/postremove.sh new file mode 100755 index 0000000..44d2528 --- /dev/null +++ b/parsec/postremove.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/parsec/stapler-repo.toml b/parsec/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/parsec/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/pineconemc/.stapler/update-check b/pineconemc/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/pineconemc/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/pineconemc/LICENSE b/pineconemc/LICENSE new file mode 100644 index 0000000..0e5fa1e --- /dev/null +++ b/pineconemc/LICENSE @@ -0,0 +1,15 @@ +Сведения о лицензии пакета PineconeMC + +Лицензия: GPL-3.0-only. +Исходный проект: https://github.com/ElyPrismLauncher/Launcher +Официальный сайт: https://pineconemc.com/ + +Нюансы пакета: +- Пакет перепаковывает официальный portable-архив PineconeMC для Linux. +- PineconeMC является форком Prism Launcher и распространяется на условиях GNU GPL версии 3. +- Пакет удаляет маркер portable-режима из исходной сборки, чтобы приложение использовало пользовательский каталог данных, а не пыталось писать в `/opt`. +- Minecraft, моды, аккаунты, серверы, Ely.by и сторонние сервисы в пакет не входят и регулируются собственными условиями. +- При распространении измененных сборок нужно соблюдать требования GPL-3.0-only. + +Полный официальный текст GPL-3.0: +https://www.gnu.org/licenses/gpl-3.0.txt diff --git a/pineconemc/Staplerfile b/pineconemc/Staplerfile new file mode 100644 index 0000000..0196bbc --- /dev/null +++ b/pineconemc/Staplerfile @@ -0,0 +1,102 @@ +name='pineconemc' +version='11.0.3' +release=3 +summary='Prism Launcher fork with Ely.by and offline account support' +summary_ru='Форк Prism Launcher с поддержкой Ely.by и offline-аккаунтов' +group='Games/Other' +desc='PineconeMC is a Prism Launcher fork with integrated Ely.by account support, offline accounts, modpack management and Minecraft instance management.' +desc_ru='PineconeMC — форк Prism Launcher с интеграцией аккаунтов Ely.by, offline-аккаунтами, управлением модпаками и инстансами Minecraft.' +homepage='https://pineconemc.com/' +maintainer='PineconeMC contributors ' + +architectures=('amd64' 'arm64') + +license=('GPL-3.0-only') + +provides=() +replaces=('pineconemc') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('ca-certificates' 'libc6' 'libdbus-1-3' 'libdrm2' 'libegl1' 'libfontconfig1' 'libfreetype6' 'libgcc-s1' 'libgl1' 'libglib2.0-0' 'libgtk-3-0' 'libice6' 'libnss3' 'libsm6' 'libstdc++6' 'libudev1' 'libx11-6' 'libxcb1' 'libxcomposite1' 'libxcursor1' 'libxdamage1' 'libxext6' 'libxfixes3' 'libxi6' 'libxkbcommon0' 'libxrandr2' 'libxrender1' 'libxtst6' 'zlib1g' 'xdg-utils') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('ca-certificates' 'glibc' 'dbus-libs' 'libdrm' 'mesa-libEGL' 'fontconfig' 'freetype' 'libgcc' 'mesa-libGL' 'glib2' 'gtk3' 'libICE' 'nss' 'libSM' 'libstdc++' 'systemd-libs' 'libX11' 'libxcb' 'libXcomposite' 'libXcursor' 'libXdamage' 'libXext' 'libXfixes' 'libXi' 'libxkbcommon' 'libXrandr' 'libXrender' 'libXtst' 'zlib' 'xdg-utils') +deps_arch=('ca-certificates' 'glibc' 'dbus' 'libdrm' 'libglvnd' 'fontconfig' 'freetype2' 'gcc-libs' 'glib2' 'gtk3' 'libice' 'nss' 'libsm' 'systemd-libs' 'libx11' 'libxcb' 'libxcomposite' 'libxcursor' 'libxdamage' 'libxext' 'libxfixes' 'libxi' 'libxkbcommon' 'libxrandr' 'libxrender' 'libxtst' 'zlib' 'xdg-utils') +deps_opensuse=('ca-certificates' 'glibc' 'libdbus-1-3' 'libdrm2' 'Mesa-libEGL1' 'fontconfig' 'libfreetype6' 'libgcc_s1' 'Mesa-libGL1' 'glib2' 'gtk3' 'libICE6' 'mozilla-nss' 'libSM6' 'libstdc++6' 'libudev1' 'libX11-6' 'libxcb1' 'libXcomposite1' 'libXcursor1' 'libXdamage1' 'libXext6' 'libXfixes3' 'libXi6' 'libxkbcommon0' 'libXrandr2' 'libXrender1' 'libXtst6' 'zlib' 'xdg-utils') +deps_altlinux=('ca-certificates' 'glibc' 'libdbus' 'libdrm' 'libEGL' 'fontconfig' 'libfreetype' 'libgcc1' 'libGL' 'glib2' 'libgtk+3' 'libICE' 'libnss' 'libSM' 'libstdc++6' 'libudev0' 'libX11' 'libxcb' 'libXcomposite' 'libXcursor' 'libXdamage' 'libXext' 'libXfixes' 'libXi' 'libxkbcommon' 'libXrandr' 'libXrender' 'libXtst' 'zlib' 'xdg-utils') + +sources=( + "https://github.com/ElyPrismLauncher/Launcher/releases/download/${version}/PineconeMC-Linux-Qt6-Portable-${version}.tar.gz?~archive=false&~name=pineconemc.tar.gz" + "local:///pineconemc.svg" + 'local:///LICENSE' +) + +sources_arm64=( + "https://github.com/ElyPrismLauncher/Launcher/releases/download/${version}/PineconeMC-Linux-aarch64-Qt6-Portable-${version}.tar.gz?~archive=false&~name=pineconemc.tar.gz" + "local:///pineconemc.svg" + 'local:///LICENSE' +) + +checksums=( + 'c1693e32668a8897529f3215aa8369e2562efde84adb999a210cc613c58ee916' + '256d854b05112270847c29f31990329e880c39dba8ab06a574db9f09749e758a' + '794a4007ac816a1425a98fdddf0716728fe1e2d899c4b9c1d505c1405e111ec9' +) + +checksums_arm64=( + '0e8d02d91efd7f6f107611163ebf2801e9411c1cbda8c83fd3f6458e24130f02' + '256d854b05112270847c29f31990329e880c39dba8ab06a574db9f09749e758a' + '794a4007ac816a1425a98fdddf0716728fe1e2d899c4b9c1d505c1405e111ec9' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${pkgdir}/opt/pineconemc" + tar --no-same-owner --no-same-permissions -xzf pineconemc.tar.gz -C "${pkgdir}/opt/pineconemc" + rm -f "${pkgdir}/opt/pineconemc/portable.txt" + + chmod 755 "${pkgdir}/opt/pineconemc/bin/elyprismlauncher" + chmod 755 "${pkgdir}/opt/pineconemc/bin/elyprismlauncher_updater" + + mkdir -p "${pkgdir}/usr/bin" + ln -sf /opt/pineconemc/bin/elyprismlauncher "${pkgdir}/usr/bin/elyprismlauncher" + ln -sf /opt/pineconemc/bin/elyprismlauncher "${pkgdir}/usr/bin/pineconemc" + + install -Dm644 "${pkgdir}/opt/pineconemc/share/applications/io.github.elyprismlauncher.ElyPrismLauncher.desktop" \ + "${pkgdir}/usr/share/applications/io.github.elyprismlauncher.ElyPrismLauncher.desktop" + sed -i 's/^Categories=.*/Categories=Game;/' \ + "${pkgdir}/usr/share/applications/io.github.elyprismlauncher.ElyPrismLauncher.desktop" + install -Dm644 "${pkgdir}/opt/pineconemc/share/metainfo/io.github.elyprismlauncher.ElyPrismLauncher.metainfo.xml" \ + "${pkgdir}/usr/share/metainfo/io.github.elyprismlauncher.ElyPrismLauncher.metainfo.xml" + install -Dm644 "${pkgdir}/opt/pineconemc/share/mime/packages/io.github.elyprismlauncher.ElyPrismLauncher.xml" \ + "${pkgdir}/usr/share/mime/packages/io.github.elyprismlauncher.ElyPrismLauncher.xml" + install -Dm644 "${pkgdir}/opt/pineconemc/share/man/man6/elyprismlauncher.6" \ + "${pkgdir}/usr/share/man/man6/elyprismlauncher.6" + + cp -a "${pkgdir}/opt/pineconemc/share/icons" "${pkgdir}/usr/share/icons" + install-license "${srcdir}/LICENSE" "pineconemc/LICENSE.nivora" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/opt" \ + "/opt/pineconemc" \ + "/opt/pineconemc/**/*" \ + "/usr/share/icons/hicolor/*/apps/io.github.elyprismlauncher.ElyPrismLauncher.*" \ + "/usr/share/man/man6/elyprismlauncher.6" \ + "/usr/share/metainfo/io.github.elyprismlauncher.ElyPrismLauncher.metainfo.xml" \ + "/usr/share/mime/packages/io.github.elyprismlauncher.ElyPrismLauncher.xml" +} diff --git a/pineconemc/pineconemc.svg b/pineconemc/pineconemc.svg new file mode 100644 index 0000000..d17bd0f --- /dev/null +++ b/pineconemc/pineconemc.svg @@ -0,0 +1,59 @@ + + + + + + Prism Launcher Logo + + + + + + + + + + + + + + + + + + + + + + + Prism Launcher Logo + 19/10/2022 + + + Prism Launcher + + + + + AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke + + + https://github.com/PrismLauncher/PrismLauncher + + + Prism Launcher + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pineconemc/postinstall.sh b/pineconemc/postinstall.sh new file mode 100755 index 0000000..b1c58f5 --- /dev/null +++ b/pineconemc/postinstall.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-mime-database >/dev/null 2>&1; then + optional_refresh "MIME-базу" update-mime-database /usr/share/mime +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi + +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/pineconemc/postremove.sh b/pineconemc/postremove.sh new file mode 100755 index 0000000..b1c58f5 --- /dev/null +++ b/pineconemc/postremove.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-mime-database >/dev/null 2>&1; then + optional_refresh "MIME-базу" update-mime-database /usr/share/mime +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi + +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/pineconemc/stapler-repo.toml b/pineconemc/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/pineconemc/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/stapler-repo.toml b/stapler-repo.toml new file mode 100644 index 0000000..3789ba1 --- /dev/null +++ b/stapler-repo.toml @@ -0,0 +1,14 @@ +[repo] +minVersion = "v0.1.1" + +title = "Nivora" +summary = "Независимый каталог Linux-приложений для Stapler" +description = """ +Nivora содержит открытые рецепты упаковки приложений, +которых может не быть в системных репозиториях дистрибутива.""" +homepage = "https://github.com/Cheviiot/Nivora" +icon = "https://raw.githubusercontent.com/Cheviiot/Nivora/main/.github/assets/nivora.png" + +url = "https://github.com/Cheviiot/Nivora.git" +ref = "main" +report_url = "https://github.com/Cheviiot/Nivora/issues/new?title={{ .BasePackageName }}" diff --git a/tailscale/.stapler/update-check b/tailscale/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/tailscale/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/tailscale/LICENSE b/tailscale/LICENSE new file mode 100644 index 0000000..148e71f --- /dev/null +++ b/tailscale/LICENSE @@ -0,0 +1,15 @@ +Сведения о лицензии пакета Tailscale + +Лицензия: BSD-3-Clause. +Исходный проект: https://github.com/tailscale/tailscale +Официальный сайт: https://tailscale.com + +Нюансы пакета: +- Пакет перепаковывает официальный Linux tar.gz-архив Tailscale. +- Устанавливаются `tailscale`, `tailscaled`, systemd-сервис и стандартные файлы запуска. +- Код клиента Tailscale распространяется под BSD-3-Clause. +- Подключение к control plane Tailscale, учетные записи, ACL и коммерческие функции регулируются условиями сервиса Tailscale. +- При распространении измененных версий нужно сохранять уведомление об авторских правах и условия BSD-3-Clause. + +Полный официальный текст BSD-3-Clause: +https://opensource.org/license/bsd-3-clause diff --git a/tailscale/Staplerfile b/tailscale/Staplerfile new file mode 100644 index 0000000..58d8f43 --- /dev/null +++ b/tailscale/Staplerfile @@ -0,0 +1,84 @@ +name='tailscale' +version='1.98.9' +release=3 +summary='Mesh VPN built on WireGuard' +summary_ru='Mesh VPN на базе WireGuard' +group='Networking/Other' +desc='Tailscale is a secure mesh VPN built on WireGuard and identity-based access that connects devices and services across networks.' +desc_ru='Tailscale — безопасный mesh VPN на базе WireGuard и identity-based access для соединения устройств и сервисов между сетями.' +homepage='https://tailscale.com' +maintainer='Tailscale Inc ' + +architectures=('amd64' 'arm64') + +license=('BSD-3-Clause') + +provides=() +replaces=('tailscale') +conflicts=() + +backup=('/etc/default/tailscaled') + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=('iptables' 'iproute2' 'ca-certificates') +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=('iptables' 'iproute' 'ca-certificates') +deps_arch=('iptables' 'iproute2' 'ca-certificates') +deps_opensuse=('iptables' 'iproute2' 'ca-certificates') +deps_altlinux=('iptables' 'iproute2' 'ca-certificates') + +sources=( + "https://pkgs.tailscale.com/stable/tailscale_${version}_amd64.tgz?~archive=false&~name=tailscale.tgz" + "local:///tailscale-purge-data" + 'local:///LICENSE' +) + +sources_arm64=( + "https://pkgs.tailscale.com/stable/tailscale_${version}_arm64.tgz?~archive=false&~name=tailscale.tgz" + "local:///tailscale-purge-data" + 'local:///LICENSE' +) + +checksums=( + '11be30ad301d48f84ff52fec34f8a2f78eb3e3dee1be4e9624d19fccc8df5540' + 'fc0e166481e2f97c21dca773ad4bfe673842268935ce1628c14b96af98e70555' + '3c7a7d6d7f09890d5e682901acaeb12bda9542fc6450a709c9dc296a91e726a5' +) + +checksums_arm64=( + 'fa554ee808d7d07ee8e3ebbc0215ea087157e2a0abbf408e6e18ea7532554db6' + 'fc0e166481e2f97c21dca773ad4bfe673842268935ce1628c14b96af98e70555' + '3c7a7d6d7f09890d5e682901acaeb12bda9542fc6450a709c9dc296a91e726a5' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + tar -xzf tailscale.tgz + src="$(find "${srcdir}" -maxdepth 1 -type d -name "tailscale_${version}_*" | head -1)" + + install-binary "${src}/tailscale" + install-binary "${srcdir}/tailscale-purge-data" + install -Dm755 "${src}/tailscaled" "${pkgdir}/usr/sbin/tailscaled" + install-systemd "${src}/systemd/tailscaled.service" + install-config "${src}/systemd/tailscaled.defaults" "default/tailscaled" + install-license "${srcdir}/LICENSE" "tailscale/LICENSE.nivora" +} + +files() { + files-find-binary + files-find-systemd + files-find-license + files-find \ + "/usr/sbin/tailscaled" \ + "/etc/default/tailscaled" +} diff --git a/tailscale/postinstall.sh b/tailscale/postinstall.sh new file mode 100755 index 0000000..c96f199 --- /dev/null +++ b/tailscale/postinstall.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -euo pipefail + +if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then + systemctl daemon-reload + systemctl enable --now tailscaled.service +fi + +REAL_USER="${SUDO_USER:-}" +if [[ -z "$REAL_USER" ]] && [[ -n "${PKEXEC_UID:-}" ]]; then + REAL_USER=$(id -nu "$PKEXEC_UID") +fi +if [[ -z "$REAL_USER" ]] && command -v logname &>/dev/null; then + REAL_USER=$(logname 2>/dev/null) || REAL_USER="" +fi + +if [[ -n "$REAL_USER" ]] && [[ "$REAL_USER" != "root" ]] && id "$REAL_USER" &>/dev/null && command -v tailscale &>/dev/null; then + for _ in 1 2 3 4 5; do + tailscale status --json &>/dev/null && break + sleep 1 + done + if ! tailscale set --operator="$REAL_USER"; then + echo "Предупреждение: не удалось назначить Tailscale operator для $REAL_USER." >&2 + fi +fi diff --git a/tailscale/postremove.sh b/tailscale/postremove.sh new file mode 100755 index 0000000..eccd269 --- /dev/null +++ b/tailscale/postremove.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euo pipefail + +unit=/usr/lib/systemd/system/tailscaled.service + +# В postremove unit отсутствует только после полного удаления. При обновлении +# новая версия уже вернула его на место, независимо от формата пакета. +if [[ ! -e "$unit" ]] && command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then + if systemctl is-active --quiet tailscaled.service; then + systemctl stop tailscaled.service + fi + if systemctl is-enabled --quiet tailscaled.service; then + systemctl disable tailscaled.service + fi + systemctl daemon-reload +fi diff --git a/tailscale/stapler-repo.toml b/tailscale/stapler-repo.toml new file mode 100644 index 0000000..d35f9bf --- /dev/null +++ b/tailscale/stapler-repo.toml @@ -0,0 +1 @@ +[inherit] diff --git a/tailscale/tailscale-purge-data b/tailscale/tailscale-purge-data new file mode 100755 index 0000000..fd2ab6a --- /dev/null +++ b/tailscale/tailscale-purge-data @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euo pipefail + +if [[ "${1:-}" != "--yes" || "$#" -ne 1 ]]; then + echo "Использование: sudo tailscale-purge-data --yes" >&2 + echo "Команда выйдет из сети Tailscale и безвозвратно удалит локальное состояние." >&2 + exit 2 +fi + +if [[ "${EUID}" -ne 0 ]]; then + echo "Ошибка: очистку данных Tailscale нужно запускать от root." >&2 + exit 1 +fi + +if command -v tailscale >/dev/null 2>&1; then + if ! tailscale logout; then + echo "Предупреждение: Tailscale не подтвердил logout; локальные данные всё равно будут удалены." >&2 + fi +fi + +if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then + systemctl stop tailscaled.service +fi + +rm -rf -- \ + /var/lib/tailscale \ + /var/cache/tailscale \ + /var/run/tailscale \ + /run/tailscale \ + /etc/tailscale +rm -f -- /var/log/tailscaled.log /var/log/tailscale.log + +echo "Локальные данные Tailscale удалены." diff --git a/tests/test_claude_alt.sh b/tests/test_claude_alt.sh new file mode 100755 index 0000000..fe1ca92 --- /dev/null +++ b/tests/test_claude_alt.sh @@ -0,0 +1,78 @@ +#!/bin/bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +launcher="${repo_root}/claude-desktop/claude-alt" +icon="${repo_root}/claude-desktop/claude-alt.png" +primary_tray_icon="${repo_root}/claude-desktop/claude-tray-orange.png" +alternate_tray_icon="${repo_root}/claude-desktop/claude-alt-tray-turquoise.png" +temp_dir="$(mktemp -d)" + +cleanup() { + find "$temp_dir" -mindepth 1 -delete + rmdir "$temp_dir" +} +trap cleanup EXIT + +mkdir -p "${temp_dir}/bin" "${temp_dir}/home" +log="${temp_dir}/launch.log" + +cat >"${temp_dir}/bin/claude-alt-bin" <<'EOF' +#!/bin/sh +set -eu +printf 'desktop=%s\n' "${CHROME_DESKTOP:-}" >"$NIVORA_TEST_LOG" +printf 'arg=%s\n' "$@" >>"$NIVORA_TEST_LOG" +EOF +chmod 0755 "${temp_dir}/bin/claude-alt-bin" + +export HOME="${temp_dir}/home" +export CLAUDE_ALT_EXECUTABLE="${temp_dir}/bin/claude-alt-bin" +export NIVORA_TEST_LOG="$log" + +unset XDG_CONFIG_HOME CLAUDE_ALT_DATA_DIR CLAUDE_DESKTOP_ACCOUNT2_DIR +"$launcher" 'claude://claude.ai/new' +grep -Fxq 'desktop=com.anthropic.ClaudeAlt.desktop' "$log" +grep -Fxq "arg=--user-data-dir=${HOME}/.config/ClaudeAlt" "$log" +grep -Fxq 'arg=--class=com.anthropic.ClaudeAlt' "$log" +grep -Fxq 'arg=claude://claude.ai/new' "$log" +test -d "${HOME}/.config/ClaudeAlt" + +legacy_config="${temp_dir}/legacy-config" +mkdir -p "${legacy_config}/Claude-Account-2" +export XDG_CONFIG_HOME="$legacy_config" +"$launcher" +grep -Fxq "arg=--user-data-dir=${legacy_config}/Claude-Account-2" "$log" + +explicit_profile="${temp_dir}/explicit-profile" +export CLAUDE_ALT_DATA_DIR="$explicit_profile" +"$launcher" +grep -Fxq "arg=--user-data-dir=${explicit_profile}" "$log" +test -d "$explicit_profile" + +python3 - "$icon" "$primary_tray_icon" "$alternate_tray_icon" <<'PY' +import struct +import sys +from pathlib import Path + +expected_sizes = { + Path(sys.argv[1]): (512, 512), + Path(sys.argv[2]): (64, 64), + Path(sys.argv[3]): (64, 64), +} + +for path, expected_size in expected_sizes.items(): + data = path.read_bytes() + if data[:8] != b"\x89PNG\r\n\x1a\n": + raise SystemExit(f"{path.name} is not a PNG") + + width, height, bit_depth, color_type = struct.unpack(">IIBB", data[16:26]) + if (width, height) != expected_size: + raise SystemExit( + f"{path.name} must be {expected_size[0]}x{expected_size[1]}, " + f"got {width}x{height}" + ) + if bit_depth != 8 or color_type not in (4, 6): + raise SystemExit(f"{path.name} must be an 8-bit PNG with alpha") +PY + +echo 'OK: ClaudeAlt launcher' diff --git a/tests/test_codex_computer_use.sh b/tests/test_codex_computer_use.sh new file mode 100755 index 0000000..57d00f4 --- /dev/null +++ b/tests/test_codex_computer_use.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +recipe="${repo_root}/codex/Staplerfile" +driver="${repo_root}/codex/patch-computer-use.js" + +grep -Fq 'release=1' "$recipe" +grep -Fq '15f3bd4fabc44bc0afba85b16d0121f0b4abd052' "$recipe" +grep -Fq 'computer-use-linux/archive/refs/tags/v0.4.1.tar.gz' "$recipe" +grep -Fq 'cargo build' "$recipe" +grep -Fq 'CODEX_LINUX_ENABLE_COMPUTER_USE_UI = "1"' "$driver" +grep -Fq 'applyLinuxComputerUseHostPlatformPatch' "$driver" +grep -Fq 'applyLinuxComputerUseInstallFlowPatch' "$driver" +grep -Fq 'codexLinuxNativeDesktopApps(' "$driver" +grep -Fq 'codexLinuxRegisterComputerUseCursorHandler' "$driver" +grep -Fq 'isHostCompatiblePlatform:' "$driver" +grep -Fq 'computer-use@openai-bundled' "${repo_root}/docs/packages/codex.md" + +python3 - "$repo_root" <<'PY' +import hashlib +import importlib.util +import json +import struct +import sys +import tempfile +from pathlib import Path + +root = Path(sys.argv[1]) +spec = importlib.util.spec_from_file_location( + "asar_tree", root / "codex" / "asar_tree.py" +) +module = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(module) + +with tempfile.TemporaryDirectory() as temporary: + work = Path(temporary) + content = b"upstream Codex fixture\n" + digest = hashlib.sha256(content).hexdigest() + header = { + "files": { + "fixture.txt": { + "size": len(content), + "offset": "0", + "integrity": { + "algorithm": "SHA256", + "hash": digest, + "blockSize": 4194304, + "blocks": [digest], + }, + } + } + } + encoded = json.dumps(header, separators=(",", ":")).encode() + padded = module.align4(len(encoded)) + payload_size = 4 + padded + archive = work / "fixture.asar" + archive.write_bytes( + struct.pack(""${temp_dir}/bin/stplr" <<'EOF' +#!/bin/bash +printf 'stplr' >>"$NIVORA_TEST_LOG" +printf ' %s' "$@" >>"$NIVORA_TEST_LOG" +printf '\n' >>"$NIVORA_TEST_LOG" +EOF + +cat >"${temp_dir}/bin/test-sudo" <<'EOF' +#!/bin/bash +printf 'sudo' >>"$NIVORA_TEST_LOG" +printf ' %s' "$@" >>"$NIVORA_TEST_LOG" +printf '\n' >>"$NIVORA_TEST_LOG" +exec "$@" +EOF + +chmod 0755 "${temp_dir}/bin/"* +for alias in sli slii sl; do + ln -s "$repo_root/nivora-stplr/nivora-stplr" "${temp_dir}/bin/${alias}" +done + +export NIVORA_TEST_LOG="$log" +export NIVORA_STPLR_SUDO=test-sudo +export NIVORA_STPLR_QUIET=1 +export PATH="${temp_dir}/bin:${PATH}" + +sli --repo nivora parsec --clean +slii nivora/codex + +cat >"${temp_dir}/expected" <<'EOF' +sudo stplr install nivora/parsec --clean +stplr install nivora/parsec --clean +stplr info nivora/codex +EOF + +diff -u "${temp_dir}/expected" "$log" +echo 'OK: Nivora helper' diff --git a/tests/test_validate_repo.py b/tests/test_validate_repo.py new file mode 100644 index 0000000..1389b97 --- /dev/null +++ b/tests/test_validate_repo.py @@ -0,0 +1,57 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +MODULE_PATH = Path(__file__).resolve().parents[1] / "tools/validate_repo.py" +SPEC = importlib.util.spec_from_file_location("validate_repo", MODULE_PATH) +assert SPEC and SPEC.loader +VALIDATOR = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(VALIDATOR) + + +class ParserTests(unittest.TestCase): + def test_scalar_and_array(self): + text = "name='demo'\narchitectures=('amd64' 'arm64')\n" + self.assertEqual(VALIDATOR.scalar(text, "name"), "demo") + self.assertEqual( + VALIDATOR.array(text, "architectures"), ["amd64", "arm64"] + ) + + def test_source_arrays_include_architectures(self): + text = "sources=('one')\n\nsources_arm64=(\n 'two'\n)\n" + self.assertEqual( + VALIDATOR.source_arrays(text), + {"sources": ["one"], "sources_arm64": ["two"]}, + ) + + def test_local_source_rejects_traversal(self): + self.assertEqual(VALIDATOR.local_source_name("local:///LICENSE"), "LICENSE") + self.assertEqual(VALIDATOR.local_source_name("local:///../secret"), "") + self.assertIsNone(VALIDATOR.local_source_name("https://example.com/file")) + + def test_markdown_targets(self): + text = "[Doc](docs/guide.md) " + self.assertEqual( + VALIDATOR.markdown_targets(text), {"docs/guide.md", "assets/icon.svg"} + ) + + +class LinkTests(unittest.TestCase): + def test_missing_link_is_reported(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "README.md" + path.write_text("[missing](no.md)\n", encoding="utf-8") + old_root = VALIDATOR.ROOT + VALIDATOR.ROOT = Path(directory) + try: + errors = [] + VALIDATOR.validate_links(path, errors) + self.assertEqual(len(errors), 1) + finally: + VALIDATOR.ROOT = old_root + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ventoy.sh b/tests/test_ventoy.sh new file mode 100755 index 0000000..4356e91 --- /dev/null +++ b/tests/test_ventoy.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +package_dir="${repo_root}/ventoy" +launcher="${package_dir}/ventoy" +desktop="${package_dir}/ventoy.desktop" +recipe="${package_dir}/Staplerfile" +temp_dir="$(mktemp -d)" + +cleanup() { + find "$temp_dir" -mindepth 1 -delete + rmdir "$temp_dir" +} +trap cleanup EXIT + +case "$(uname -m)" in +x86_64 | amd64) + gui='VentoyGUI.x86_64' + ;; +aarch64 | arm64) + gui='VentoyGUI.aarch64' + ;; +*) + echo 'SKIP: архитектура не поддерживается тестом Ventoy' + exit 0 + ;; +esac + +cat >"${temp_dir}/${gui}" <<'EOF' +#!/bin/bash +set -euo pipefail +printf 'cwd=%s\n' "$PWD" >"$NIVORA_TEST_LOG" +printf 'arg=%s\n' "$@" >>"$NIVORA_TEST_LOG" +EOF +chmod 0755 "${temp_dir}/${gui}" + +export VENTOY_INSTALL_DIR="$temp_dir" +export NIVORA_TEST_LOG="${temp_dir}/launch.log" +"$launcher" --qt5 + +grep -Fxq "cwd=${temp_dir}" "$NIVORA_TEST_LOG" +grep -Fxq 'arg=--qt5' "$NIVORA_TEST_LOG" +grep -Fxq 'Name=Ventoy' "$desktop" +grep -Fxq 'Exec=ventoy' "$desktop" +grep -Fxq 'Icon=ventoy' "$desktop" +grep -Fq "architectures=('amd64' 'arm64')" "$recipe" +grep -Fq "VentoyGUI.\${candidate}" "$recipe" +grep -Fq "case \"\$(uname -m)\" in" "$recipe" + +echo 'OK: Ventoy launcher' diff --git a/tests/test_yandex_browser.sh b/tests/test_yandex_browser.sh new file mode 100755 index 0000000..8c37b89 --- /dev/null +++ b/tests/test_yandex_browser.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +recipe="${repo_root}/yandex-browser-stable/Staplerfile" +postinstall="${repo_root}/yandex-browser-stable/postinstall.sh" + +grep -Fq 'https://repo.yandex.ru/yandex-browser/deb/' "$recipe" +grep -Fq '/usr/bin/yandex-browser-stable' "$recipe" +grep -Fq 'extracted/usr/share/applications' "$recipe" +grep -Fq 'compatibility_desktop=' "$recipe" +grep -Fq 'canonical_desktop=' "$recipe" +grep -Fq "sed -i '/^NoDisplay=true\$/d'" "$recipe" +grep -Fq "sed -i '/^\\[Desktop Entry\\]\$/a NoDisplay=true'" "$recipe" +grep -Fq 'update_codecs' "$postinstall" + +if grep -Eq '/etc/(cron|xdg/autostart)' "$recipe"; then + echo 'Yandex Browser recipe must not package upstream auto-update hooks' >&2 + exit 1 +fi + +echo 'OK: Yandex Browser управляется Stapler без upstream cron-задач' diff --git a/tools/autonomous_package_updates.sh b/tools/autonomous_package_updates.sh new file mode 100755 index 0000000..d226052 --- /dev/null +++ b/tools/autonomous_package_updates.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -uo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +results_dir="${AUTONOMOUS_UPDATE_RESULTS_DIR:?AUTONOMOUS_UPDATE_RESULTS_DIR is required}" + +[[ "$#" -gt 0 ]] || { + echo 'usage: autonomous_package_updates.sh package...' >&2 + exit 2 +} + +mkdir -p "$results_dir" +: >"${results_dir}/successful-packages" +: >"${results_dir}/failed-packages" + +for package in "$@"; do + package_result="${results_dir}/${package}" + worktree="${RUNNER_TEMP:?RUNNER_TEMP is required}/update-${package}" + mkdir -p "$package_result" + + if ! git -C "$repo_root" worktree add --detach "$worktree" HEAD \ + >"${package_result}/worktree.log" 2>&1; then + { + printf 'package=%s\n' "$package" + printf 'phase=prepare-worktree\n' + printf 'exit_status=1\n' + } >"${package_result}/FAILED" + printf '%s\t%s\n' "$package" prepare-worktree \ + >>"${results_dir}/failed-packages" + echo failure >"${package_result}/result" + continue + fi + + phase_file="${package_result}/phase" + log_file="${package_result}/update.log" + if ( + set -euo pipefail + cd "$worktree" + + echo update-recipe >"$phase_file" + stplr-spec update-package "$package" + + echo sync-catalog >"$phase_file" + tools/sync_readme_versions.py + + echo static-checks >"$phase_file" + tools/run_checks.sh + + echo clean-build >"$phase_file" + tools/clean_build.sh "$package" + + echo verify-artifact >"$phase_file" + tools/verify_artifacts.sh "$package" + ) >"$log_file" 2>&1; then + git -C "$worktree" diff --binary -- "$package" \ + >"${package_result}/update.patch" + printf '%s\n' "$package" >>"${results_dir}/successful-packages" + echo success >"${package_result}/result" + else + status=$? + failed_phase="$(<"$phase_file")" + { + printf 'package=%s\n' "$package" + printf 'phase=%s\n' "$failed_phase" + printf 'exit_status=%s\n' "$status" + } >"${package_result}/FAILED" + git -C "$worktree" diff --binary \ + >"${package_result}/failed.patch" + if [[ -f "${worktree}/${package}/Staplerfile" ]]; then + install -Dm644 "${worktree}/${package}/Staplerfile" \ + "${package_result}/Staplerfile.after" + fi + printf '%s\t%s\n' "$package" "$failed_phase" \ + >>"${results_dir}/failed-packages" + echo failure >"${package_result}/result" + fi + + git -C "$repo_root" worktree remove --force "$worktree" \ + >>"${package_result}/worktree.log" 2>&1 +done diff --git a/tools/clean_build.sh b/tools/clean_build.sh new file mode 100755 index 0000000..3b36476 --- /dev/null +++ b/tools/clean_build.sh @@ -0,0 +1,130 @@ +#!/bin/bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +# shellcheck source=tools/lib/source_cache.sh +source "${script_dir}/lib/source_cache.sh" +cd "$repo_root" + +mapfile -t all_packages < <( + for staplerfile in */Staplerfile; do + dirname "$staplerfile" + done | sort +) + +is_package() { + local requested="$1" + local package + for package in "${all_packages[@]}"; do + [[ "$requested" == "$package" ]] && return 0 + done + return 1 +} + +packages=() +if [[ "${1:-}" == '--all' ]]; then + packages=("${all_packages[@]}") + shift +else + packages=("$@") +fi + +[[ "${#packages[@]}" -gt 0 ]] || { + echo 'Usage: tools/clean_build.sh {--all|package...}' >&2 + exit 2 +} + +for package in "${packages[@]}"; do + is_package "$package" || { + echo "Unknown package: ${package}" >&2 + exit 2 + } +done + +if command -v podman >/dev/null 2>&1; then + engine=podman +elif command -v docker >/dev/null 2>&1; then + engine=docker +else + echo 'clean-build requires stplr-spec with clean-build, Podman or Docker' >&2 + exit 2 +fi + +readonly image='registry.altlinux.org/sisyphus/base:latest' +readonly cache_volume="nivora-clean-build-cache-$$" +readonly builder_image="nivora-clean-build:$$" +builder_dir="$(mktemp -d)" +docker_config='' +source_cache="${NIVORA_SOURCE_CACHE:-${XDG_CACHE_HOME:-${HOME}/.cache}/stplr/dl}" + +cleanup() { + "$engine" volume rm "$cache_volume" >/dev/null 2>&1 || true + "$engine" image rm "$builder_image" >/dev/null 2>&1 || true + find "$builder_dir" -mindepth 1 -delete + rmdir "$builder_dir" + if [[ -n "$docker_config" ]]; then + find "$docker_config" -mindepth 1 -delete + rmdir "$docker_config" + fi +} +trap cleanup EXIT + +if [[ "$engine" == docker ]]; then + docker_config="$(mktemp -d)" + printf '{}\n' >"${docker_config}/config.json" + export DOCKER_CONFIG="$docker_config" +fi + +cat >"${builder_dir}/Containerfile" </dev/null +"$engine" build -t "$builder_image" -f "${builder_dir}/Containerfile" "$builder_dir" + +import_stplr_source_cache \ + "$engine" "$builder_image" "$cache_volume" "$builder_dir" "$source_cache" \ + "${packages[@]}" + +for package in "${packages[@]}"; do + echo "==> clean-build ${package} (${engine})" + find "$package" -maxdepth 1 -type f \ + \( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.pkg.tar.*' \) \ + -delete + built=0 + for attempt in 1 2; do + if "$engine" run --rm --privileged \ + -e TERM=xterm-256color \ + -v "${repo_root}/${package}:/app" \ + -v "${cache_volume}:/var/cache/stplr" \ + -w /app \ + "$builder_image" \ + stplr --interactive=false build --clean -s Staplerfile; then + built=1 + break + fi + echo "==> retry ${package} (${attempt}/2)" >&2 + sleep "$((attempt * 5))" + done + [[ "$built" -eq 1 ]] || exit 1 +done + +echo "OK: clean-build завершён для ${#packages[@]} пакетов" diff --git a/tools/lib/source_cache.sh b/tools/lib/source_cache.sh new file mode 100755 index 0000000..80d7004 --- /dev/null +++ b/tools/lib/source_cache.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +import_stplr_source_cache() { + local cache_engine="$1" + local cache_image="$2" + local cache_volume_name="$3" + local cache_work_dir="$4" + local cache_source_dir="$5" + shift 5 + local packages=("$@") + + if [[ ! -d "$cache_source_dir" ]] || ! command -v sqlite3 >/dev/null 2>&1; then + echo '==> source cache import skipped' + return 0 + fi + + local cache_manifest="${cache_work_dir}/source-cache.tsv" + local cache_db="${cache_work_dir}/source-cache.db" + local package package_name package_version index source expected + local url_hash restore_name candidate actual + local -a package_sources package_checksums + + : >"$cache_manifest" + sqlite3 "$cache_db" ' + CREATE TABLE cache_record ( + i_d INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + hash TEXT NULL, + repo TEXT NULL, + pkg TEXT NULL, + ver TEXT NULL, + name TEXT NULL, + type INTEGER NULL + ); + CREATE INDEX IDX_cache_record_hash ON cache_record(hash); + CREATE INDEX IDX_cache_record_repo ON cache_record(repo); + CREATE INDEX IDX_cache_record_pkg ON cache_record(pkg); + CREATE INDEX IDX_cache_record_ver ON cache_record(ver); + ' + + for package in "${packages[@]}"; do + package_name="$(stplr-spec get-field --path "${package}/Staplerfile" name)" + package_version="$(stplr-spec get-field --path "${package}/Staplerfile" version)" + [[ "$package_name" =~ ^[a-z0-9.+-]+$ ]] + [[ "$package_version" =~ ^[A-Za-z0-9._+~-]+$ ]] + + read -ra package_sources <<<"$( + stplr-spec get-field --path "${package}/Staplerfile" sources + )" + read -ra package_checksums <<<"$( + stplr-spec get-field --path "${package}/Staplerfile" checksums + )" + + for index in "${!package_sources[@]}"; do + source="${package_sources[$index]}" + [[ "$source" != local://* && "$source" == *'~archive=false'* ]] || continue + [[ -v 'package_checksums[index]' ]] + expected="${package_checksums[$index]#sha256:}" + url_hash="$(printf '%s' "$source" | sha256sum)" + url_hash="${url_hash%% *}" + if [[ "$source" == *'~name='* ]]; then + restore_name="${source##*~name=}" + restore_name="${restore_name%%&*}" + else + restore_name="$(basename "${source%%\?*}")" + fi + [[ "$restore_name" =~ ^[A-Za-z0-9._+-]+$ ]] + + candidate="${cache_source_dir}/${url_hash}/${restore_name}" + [[ -f "$candidate" ]] || continue + actual="$(sha256sum "$candidate")" + actual="${actual%% *}" + [[ "$actual" == "$expected" ]] || continue + + printf '%s\t%s\t%s\n' "$url_hash" "$restore_name" "$expected" \ + >>"$cache_manifest" + sqlite3 "$cache_db" \ + "INSERT INTO cache_record(hash, repo, pkg, ver, name, type) VALUES('$url_hash', 'default', '$package_name', '$package_version', '$restore_name', 1);" + done + done + + echo "==> importing verified sources from ${cache_source_dir}" + # The script is intentionally expanded inside the container, not by the host shell. + # shellcheck disable=SC2016 + "$cache_engine" run --rm \ + -v "${cache_source_dir}:/source:ro" \ + -v "${cache_manifest}:/manifest:ro" \ + -v "${cache_db}:/cache-db:ro" \ + -v "${cache_volume_name}:/var/cache/stplr" \ + "$cache_image" \ + bash -euo pipefail -c ' + mkdir -p /var/cache/stplr/dl + cp -a /cache-db /var/cache/stplr/dl/db + imported=0 + bytes=0 + while IFS=$'"'"'\t'"'"' read -r url_hash restore_name expected; do + source="/source/${url_hash}/${restore_name}" + actual="$(sha256sum "$source")" + actual="${actual%% *}" + [[ "$actual" == "$expected" ]] + destination="/var/cache/stplr/dl/${url_hash}/${restore_name}" + mkdir -p "${destination%/*}" + cp -a "$source" "$destination" + size="$(stat -c %s "$source")" + imported=$((imported + 1)) + bytes=$((bytes + size)) + done &2 + exit 1 +} + +require_package() { + local requested="$1" + local package + for package in "${PACKAGES[@]}"; do + [[ "$package" == "$requested" ]] && return 0 + done + die "unknown package: ${requested}" +} + +current_version() { + stplr-spec get-field --path "${repo_root}/$1/Staplerfile" version +} + +github_json() { + local url="$1" + local -a headers=( + -H 'Accept: application/vnd.github+json' + -H 'X-GitHub-Api-Version: 2022-11-28' + ) + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + headers+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") + fi + curl --retry 3 --retry-delay 2 --retry-all-errors \ + --connect-timeout 30 --max-time 120 -fsSL "${headers[@]}" "$url" +} + +git_latest_stable_tag() { + local repository="$1" + local attempt tag='' + + for attempt in 1 2 3; do + tag="$( + GIT_TERMINAL_PROMPT=0 timeout 60s \ + git ls-remote --tags --refs "https://github.com/${repository}.git" 'refs/tags/v*' | + awk '{sub("refs/tags/", "", $2); print $2}' | + grep -Eiv '(^|[-_.])(alpha|beta|rc|pre|preview)([-_.0-9]|$)' | + sort -V | + tail -1 + )" || tag='' + if [[ -n "$tag" ]]; then + printf '%s\n' "$tag" + return 0 + fi + sleep "$((attempt * 2))" + done + + return 1 +} + +github_latest_release() { + local repository="$1" + local tag='' + tag="$( + github_json "https://api.github.com/repos/${repository}/releases?per_page=30" | + jq -r ' + [ + .[] + | select(.draft == false and .prerelease == false) + | .tag_name + | select(test("(?:^|[-_.])(alpha|beta|rc|pre|preview)(?:[-_.0-9]|$)"; "i") | not) + ][0] // empty + ' 2>/dev/null + )" || tag='' + + if [[ -z "$tag" ]]; then + tag="$(git_latest_stable_tag "$repository")" || tag='' + fi + + [[ -n "$tag" ]] || { + echo "package_updates: cannot determine latest release for ${repository}" >&2 + return 1 + } + printf '%s\n' "${tag#v}" +} + +latest_anidesk() { + local version + version="$( + GIT_TERMINAL_PROMPT=0 timeout 60s \ + git ls-remote --tags --refs https://github.com/theDesConnet/AniDesk.git 'refs/tags/v*' | + awk '{sub("refs/tags/v", "", $2); print $2}' | + sort -V | + tail -1 + )" + [[ -n "$version" ]] || die 'cannot determine latest AniDesk version' + printf '%s\n' "$version" +} + +latest_chatbox() { + local version + version="$( + github_json 'https://api.github.com/repos/chatboxai/chatbox/releases?per_page=30' | + jq -er ' + [ + .[] + | select(.draft == false and .prerelease == false) + | . as $release + | $release.assets[]? + | select(.name | test("^Chatbox-[0-9]+(?:\\.[0-9]+)+-amd64\\.deb$")) + | $release.tag_name + ][0] + ' | + sed 's/^v//' + )" || die 'cannot determine latest Chatbox Linux release' + printf '%s\n' "$version" +} + +latest_claude_desktop() { + local version + version="$( + curl --retry 3 --retry-delay 2 --retry-all-errors \ + --connect-timeout 30 --max-time 120 -fsSL \ + 'https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages' | + awk ' + /^Package: claude-desktop$/ { selected = 1; next } + /^Package: / { selected = 0 } + selected && /^Version: / { print $2 } + ' | + sort -V | + tail -1 + )" + [[ -n "$version" ]] || die 'cannot determine latest Claude Desktop version' + printf '%s\n' "$version" +} + +latest_parsec() { + local temp_dir version + temp_dir="$(mktemp -d)" + curl --retry 3 --retry-delay 2 --retry-all-errors \ + --connect-timeout 30 --max-time 300 -fsSL \ + -o "${temp_dir}/parsec.deb" \ + 'https://builds.parsec.app/package/parsec-linux.deb' + ( + cd "$temp_dir" + ar x parsec.deb + tar -xOf control.tar.* ./control + ) >"${temp_dir}/control" + version="$(awk '$1 == "Version:" {print $2; exit}' "${temp_dir}/control")" + [[ -n "$version" ]] || die 'cannot determine latest Parsec version' + printf '%s\n' "$version" + find "$temp_dir" -mindepth 1 -delete + rmdir "$temp_dir" +} + +latest_tailscale() { + local effective version + effective="$( + curl --retry 3 --retry-delay 2 --retry-all-errors \ + --connect-timeout 30 --max-time 120 -fsSLI -o /dev/null \ + -w '%{url_effective}' \ + 'https://pkgs.tailscale.com/stable/tailscale_latest_amd64.tgz' + )" + version="$(sed -n 's/.*tailscale_\([0-9][0-9.]*\)_amd64\.tgz.*/\1/p' <<<"$effective")" + [[ -n "$version" ]] || die 'cannot determine latest Tailscale version' + printf '%s\n' "$version" +} + +latest_yandex_browser() { + local version + version="$( + curl --retry 3 --retry-delay 2 --retry-all-errors \ + --connect-timeout 30 --max-time 120 -fsSL \ + 'https://repo.yandex.ru/yandex-browser/deb/dists/stable/main/binary-amd64/Packages.gz' | + gzip -dc | + awk ' + /^Package: yandex-browser-stable$/ { selected = 1; next } + /^Package: / { selected = 0 } + selected && /^Version: / { + version = $2 + sub(/-[^-]+$/, "", version) + print version + } + ' | + sort -V | + tail -1 + )" + [[ -n "$version" ]] || die 'cannot determine latest Yandex Browser version' + printf '%s\n' "$version" +} + +latest_version() { + case "$1" in + adwyra) github_latest_release Cheviiot/Adwyra ;; + anidesk) latest_anidesk ;; + balena-etcher) github_latest_release balena-io/etcher ;; + chatbox) latest_chatbox ;; + clash-verge-rev) github_latest_release clash-verge-rev/clash-verge-rev ;; + claude-desktop) latest_claude_desktop ;; + codex) github_latest_release Boria138/codex-app-linux ;; + fisher) github_latest_release jorgebucaran/fisher ;; + github-desktop) + github_latest_release desktop/desktop | sed 's/^release-//' + ;; + happ) github_latest_release Happ-proxy/happ-desktop ;; + netbird) github_latest_release netbirdio/netbird ;; + nivora-stplr) current_version nivora-stplr ;; + opencode) github_latest_release anomalyco/opencode ;; + parsec) latest_parsec ;; + pineconemc) github_latest_release ElyPrismLauncher/Launcher ;; + tailscale) latest_tailscale ;; + ventoy) github_latest_release ventoy/Ventoy ;; + vual) github_latest_release Cheviiot/Vual ;; + yandex-browser-stable) latest_yandex_browser ;; + *) die "unknown package: $1" ;; + esac +} + +check_package() { + local package="$1" + local current latest + require_package "$package" + current="$(current_version "$package")" || return + latest="$(latest_version "$package")" || return + printf '%s %s\n' "$current" "$latest" +} + +check_all() { + local package current latest status versions + local updates=0 + printf '%-24s %-24s %-24s %s\n' PACKAGE CURRENT LATEST STATUS + for package in "${PACKAGES[@]}"; do + versions="$(check_package "$package")" || return + read -r current latest <<<"$versions" + status=current + if [[ "$current" != "$latest" ]]; then + status=update + updates=1 + fi + printf '%-24s %-24s %-24s %s\n' "$package" "$current" "$latest" "$status" + done + [[ "$updates" -eq 0 ]] || return 10 +} + +outdated_packages() { + local package current latest versions + for package in "${PACKAGES[@]}"; do + versions="$(check_package "$package")" || return + read -r current latest <<<"$versions" + if [[ "$current" != "$latest" ]]; then + printf '%s\n' "$package" + fi + done +} + +case "${1:-}" in +check) + [[ "$#" -eq 2 ]] || die 'usage: package_updates.sh check ' + check_package "$2" + ;; +check-all) + [[ "$#" -eq 1 ]] || die 'usage: package_updates.sh check-all' + check_all + ;; +outdated) + [[ "$#" -eq 1 ]] || die 'usage: package_updates.sh outdated' + outdated_packages + ;; +*) + die 'usage: package_updates.sh {check |check-all|outdated}' + ;; +esac diff --git a/tools/report_update_failures.sh b/tools/report_update_failures.sh new file mode 100755 index 0000000..6cbc0bf --- /dev/null +++ b/tools/report_update_failures.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -euo pipefail + +results_dir="${AUTONOMOUS_UPDATE_RESULTS_DIR:?AUTONOMOUS_UPDATE_RESULTS_DIR is required}" +repository="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +run_url="https://github.com/${repository}/actions/runs/${GITHUB_RUN_ID:?GITHUB_RUN_ID is required}" +issues_json="$( + gh api --paginate --slurp \ + "repos/${repository}/issues?state=all&per_page=100" +)" +code_fence='```' + +find_issue() { + local marker="$1" + jq -r --arg marker "$marker" ' + [ + .[][] + | select(has("pull_request") | not) + | select((.body // "") | contains($marker)) + ][0].number // empty + ' <<<"$issues_json" +} + +while IFS=$'\t' read -r package phase; do + [[ -n "$package" ]] || continue + marker="" + issue_number="$(find_issue "$marker")" + report="${results_dir}/${package}/issue-body.md" + { + printf '%s\n\n' "$marker" + printf 'Автономное обновление пакета **%s** остановлено на фазе **%s**.\n\n' \ + "$package" "$phase" + printf -- '- Последний запуск: %s\n' "$run_url" + printf -- '- Диагностика Actions хранится 30 дней в artifact запуска.\n' + printf -- '- Остальные пакеты обновляются независимо от этого сбоя.\n\n' + printf '### Состояние\n\n%stext\n' "$code_fence" + sed -n '1,80p' "${results_dir}/${package}/FAILED" + printf '%s\n\n### Последние строки журнала\n\n%stext\n' \ + "$code_fence" "$code_fence" + tail -n 80 "${results_dir}/${package}/update.log" 2>/dev/null || true + printf '%s\n\n### Изменения рецепта\n\n%sdiff\n' \ + "$code_fence" "$code_fence" + sed -n '1,200p' "${results_dir}/${package}/failed.patch" \ + 2>/dev/null || true + printf '%s\n' "$code_fence" + } >"$report" + + if [[ -n "$issue_number" ]]; then + gh api --method PATCH "repos/${repository}/issues/${issue_number}" \ + -f state=open \ + -f body="$(<"$report")" >/dev/null + else + gh api --method POST "repos/${repository}/issues" \ + -f title="[autoupdate] ${package}: требуется диагностика" \ + -f body="$(<"$report")" >/dev/null + fi +done <"${results_dir}/failed-packages" + +while IFS= read -r package; do + [[ -n "$package" ]] || continue + marker="" + issue_number="$(find_issue "$marker")" + if [[ -n "$issue_number" ]]; then + gh api --method PATCH "repos/${repository}/issues/${issue_number}" \ + -f state=closed \ + -f state_reason=completed >/dev/null + fi +done <"${results_dir}/successful-packages" diff --git a/tools/run_checks.sh b/tools/run_checks.sh new file mode 100755 index 0000000..197a54e --- /dev/null +++ b/tools/run_checks.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +cd "$repo_root" + +mapfile -d '' shell_files < <( + find . -type f -not -path './.git/*' -print0 | + while IFS= read -r -d '' file; do + if head -n 1 -- "$file" 2>/dev/null | + grep -IqE '^#!.*(bash|/sh)([[:space:]]|$)'; then + printf '%s\0' "$file" + fi + done +) + +mapfile -d '' python_files < <( + find . -type f -name '*.py' -not -path './.git/*' -print0 +) + +for file in "${shell_files[@]}"; do + bash -n "$file" +done + +if [[ "${#shell_files[@]}" -gt 0 ]]; then + shellcheck -x "${shell_files[@]}" +fi + +if [[ "${#python_files[@]}" -gt 0 ]]; then + python3 -m py_compile "${python_files[@]}" +fi + +python3 -m unittest discover -s tests -p 'test_*.py' -v +bash tests/test_helper.sh +bash tests/test_claude_alt.sh +bash tests/test_codex_computer_use.sh +bash tests/test_happ_theme.sh +bash tests/test_ventoy.sh +bash tests/test_yandex_browser.sh +python3 tools/validate_repo.py + +if command -v stplr-spec >/dev/null 2>&1; then + for staplerfile in */Staplerfile; do + stplr-spec get-field --path "$staplerfile" name >/dev/null + done +fi + +echo 'OK: все проверки Nivora завершены' diff --git a/tools/sync_readme_versions.py b/tools/sync_readme_versions.py new file mode 100755 index 0000000..94a0583 --- /dev/null +++ b/tools/sync_readme_versions.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import re +import shlex +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +README = ROOT / "README.md" + + +def package_metadata(package: str) -> tuple[str, list[str]]: + text = (ROOT / package / "Staplerfile").read_text(encoding="utf-8") + version_match = re.search( + r"^version=(?:'([^']+)'|\"([^\"]+)\"|([^#\s]+))", + text, + re.MULTILINE, + ) + if not version_match: + raise RuntimeError(f"{package}: version is missing") + version = next( + value for value in version_match.groups() if value is not None + ) + + architectures_match = re.search( + r"^architectures=\((.*?)\)", text, re.MULTILINE | re.DOTALL + ) + if not architectures_match: + raise RuntimeError(f"{package}: architectures are missing") + architectures = shlex.split(architectures_match.group(1)) + if not architectures: + raise RuntimeError(f"{package}: architectures are empty") + + return version, architectures + + +def main() -> None: + lines = README.read_text(encoding="utf-8").splitlines() + package_dirs = sorted( + path.parent.name for path in ROOT.glob("*/Staplerfile") + ) + + for package in package_dirs: + command = f"stplr install nivora/{package}" + version, architectures = package_metadata(package) + matching_lines = [ + index + for index, line in enumerate(lines) + if line.startswith("|") and command in line + ] + if len(matching_lines) != 1: + raise RuntimeError( + f"{package}: expected one README catalog row, " + f"got {len(matching_lines)}" + ) + + index = matching_lines[0] + cells = lines[index].split("|") + if len(cells) != 6: + raise RuntimeError(f"{package}: malformed README catalog row") + cells[2] = f" `{version}` " + cells[3] = " " + ", ".join( + f"`{architecture}`" for architecture in architectures + ) + " " + lines[index] = "|".join(cells) + + README.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/tools/test_package_lifecycle.sh b/tools/test_package_lifecycle.sh new file mode 100755 index 0000000..1a36453 --- /dev/null +++ b/tools/test_package_lifecycle.sh @@ -0,0 +1,448 @@ +#!/bin/bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +# shellcheck source=tools/lib/source_cache.sh +source "${script_dir}/lib/source_cache.sh" +cd "$repo_root" + +readonly stplr_version='0.1.1' +readonly stplr_archive_url="https://altlinux.space/stapler/stplr/releases/download/v${stplr_version}/stplr-${stplr_version}-linux-x86_64.tar.gz" +readonly stplr_archive_sha256='b1ec1e98c04ab928377d0cef1706e3dc62171b9e256f4bf36a219addc53117b8' +readonly deb_image="nivora-lifecycle-deb:$$" +readonly rpm_image="nivora-lifecycle-rpm:$$" +readonly deb_cache_volume="nivora-lifecycle-deb-cache-$$" +readonly deb_build_mode="${NIVORA_DEB_BUILD_MODE:-container}" + +case "$deb_build_mode" in +container | host) ;; +*) + echo "Неизвестный режим DEB-сборки: ${deb_build_mode}" >&2 + exit 2 + ;; +esac + +# package | command | persistent state marker | desktop/unit | icon +readonly -a lifecycle_packages=( + 'clash-verge-rev|/usr/bin/clash-verge|/home/nivora-test/.local/share/io.github.clash-verge-rev.clash-verge-rev/nivora-lifecycle-state|/usr/share/applications/Clash Verge.desktop|/usr/share/icons/hicolor/128x128/apps/clash-verge.png' + 'claude-desktop|/usr/bin/claude-desktop|/home/nivora-test/.config/Claude/nivora-lifecycle-state|/usr/share/applications/com.anthropic.Claude.desktop|/usr/share/icons/hicolor/128x128/apps/claude-desktop.png' + 'codex|/usr/bin/codex-app|/home/nivora-test/.codex/nivora-lifecycle-state|/usr/share/applications/codex-app.desktop|/usr/share/icons/hicolor/512x512/apps/codex-app.png' + 'nivora-stplr|/usr/bin/sl|/home/nivora-test/.config/nivora-stplr/nivora-lifecycle-state|-|-' + 'opencode|/usr/bin/opencode-desktop|/home/nivora-test/.config/opencode/nivora-lifecycle-state|/usr/share/applications/opencode-desktop.desktop|/usr/share/icons/hicolor/128x128/apps/ai.opencode.desktop.png' + 'tailscale|/usr/bin/tailscale|/var/lib/tailscale/nivora-lifecycle-state|/usr/lib/systemd/system/tailscaled.service|-' + 'netbird|/usr/bin/netbird|/var/lib/netbird/nivora-lifecycle-state|/usr/lib/systemd/system/netbird.service|-' + 'chatbox|/usr/bin/chatbox|/home/nivora-test/.config/Chatbox/nivora-lifecycle-state|/usr/share/applications/xyz.chatboxapp.app.desktop|/usr/share/icons/hicolor/128x128/apps/xyz.chatboxapp.app.png' +) + +for command in curl docker dpkg-deb find rpm rpmbuild sha256sum sqlite3 stplr-spec tar; do + command -v "$command" >/dev/null 2>&1 || { + echo "Для lifecycle-теста требуется команда ${command}" >&2 + exit 2 + } +done + +[[ "$(uname -m)" == 'x86_64' ]] || { + echo 'Lifecycle-тест сейчас поддерживает только x86_64 runner' >&2 + exit 2 +} + +work_dir="$(mktemp -d)" +source_cache="${NIVORA_SOURCE_CACHE:-${XDG_CACHE_HOME:-${HOME}/.cache}/stplr/dl}" +apparmor_userns_original='' + +cleanup() { + docker volume rm "$deb_cache_volume" >/dev/null 2>&1 || true + docker image rm "$deb_image" >/dev/null 2>&1 || true + docker image rm "$rpm_image" >/dev/null 2>&1 || true + if [[ "$deb_build_mode" == 'host' && -d "$work_dir" ]]; then + sudo chown -R "$(id -u):$(id -g)" "$work_dir" >/dev/null 2>&1 || true + fi + if [[ -n "$apparmor_userns_original" ]]; then + sudo sysctl -q -w \ + "kernel.apparmor_restrict_unprivileged_userns=${apparmor_userns_original}" \ + >/dev/null 2>&1 || true + fi + find "$work_dir" -mindepth 1 -delete + rmdir "$work_dir" +} +trap cleanup EXIT + +install -d \ + "${work_dir}/builder" \ + "${work_dir}/fixtures/previous-deb" \ + "${work_dir}/fixtures/previous-rpm" + +prepare_stplr() { + local target="${work_dir}/builder/stplr" + local archive actual + + if command -v stplr >/dev/null 2>&1 && + [[ "$(stplr version 2>/dev/null)" == "v${stplr_version}" ]]; then + cp "$(command -v stplr)" "$target" + else + archive="${work_dir}/stplr.tar.gz" + curl -fL \ + --retry 3 \ + --retry-all-errors \ + --connect-timeout 20 \ + --max-time 300 \ + -o "$archive" \ + "$stplr_archive_url" + actual="$(sha256sum "$archive")" + actual="${actual%% *}" + [[ "$actual" == "$stplr_archive_sha256" ]] || { + echo "Неверная SHA-256 stplr: ${actual}" >&2 + exit 1 + } + tar -xzf "$archive" -C "${work_dir}/builder" stplr + fi + chmod 0755 "$work_dir" "${work_dir}/builder" "$target" +} + +build_images() { + cat >"${work_dir}/builder/Containerfile.deb" <<'EOF' +FROM ubuntu:24.04 +ARG DEBIAN_FRONTEND=noninteractive +COPY stplr /usr/local/bin/stplr +RUN set -eux; \ + success=0; \ + for attempt in 1 2 3; do \ + if apt-get -o Acquire::Retries=2 -o Acquire::http::Timeout=20 update \ + && apt-get install -y ca-certificates binutils passwd python3 xz-utils zstd; then \ + success=1; \ + break; \ + fi; \ + sleep "$((attempt * 5))"; \ + done; \ + test "$success" -eq 1; \ + useradd --system --create-home stapler-builder; \ + mkdir -p /var/cache/stplr; \ + chown -R stapler-builder:stapler-builder /var/cache/stplr +EOF + + cat >"${work_dir}/builder/Containerfile.rpm" <<'EOF' +FROM registry.altlinux.org/sisyphus/base:latest +RUN for attempt in 1 2 3; do \ + apt-get -o Acquire::Retries=2 -o Acquire::http::Timeout=20 update \ + && apt-get dist-upgrade -y \ + && apt-get install -y ca-certificates stplr binutils python3 \ + && exit 0; \ + sleep "$((attempt * 5))"; \ + done; \ + exit 1 +EOF + + docker build -t "$deb_image" -f "${work_dir}/builder/Containerfile.deb" \ + "${work_dir}/builder" + docker build -t "$rpm_image" -f "${work_dir}/builder/Containerfile.rpm" \ + "${work_dir}/builder" +} + +prepare_host_deb_builder() { + if ! getent group wheel >/dev/null; then + sudo groupadd --system wheel + fi + if ! getent passwd stapler-builder >/dev/null; then + sudo useradd --system --create-home stapler-builder + fi + sudo usermod -a -G wheel stapler-builder + sudo install -d -o stapler-builder -g stapler-builder /var/cache/stplr + sudo -u stapler-builder test -x "${work_dir}/builder/stplr" + + if [[ -r /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then + apparmor_userns_original="$( + sysctl -n kernel.apparmor_restrict_unprivileged_userns + )" + if [[ "$apparmor_userns_original" != '0' ]]; then + sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0 + else + apparmor_userns_original='' + fi + fi + + sudo -u stapler-builder unshare \ + --user --map-root-user --mount --pid --fork --uts --ipc --cgroup \ + true +} + +build_deb_on_host() { + local package="$1" + local stage="${work_dir}/host-packages/${package}" + local builder_home + local -a artifacts + + install -d "$stage" + cp -a "${package}/." "$stage/" + find "$stage" -maxdepth 1 -type f \( -name '*.deb' -o -name '*.rpm' \) -delete + sudo chown -R stapler-builder:stapler-builder "$stage" + builder_home="$(getent passwd stapler-builder | cut -d: -f6)" + + ( + cd "$stage" + sudo -u stapler-builder env \ + HOME="$builder_home" \ + TERM=xterm-256color \ + "${work_dir}/builder/stplr" \ + --interactive=false build --clean -s Staplerfile + ) + + mapfile -t artifacts < <(find "$stage" -maxdepth 1 -type f -name '*.deb' -print) + [[ "${#artifacts[@]}" -eq 1 ]] + install -m0644 "${artifacts[0]}" "${package}/$(basename "${artifacts[0]}")" +} + +mapfile -t packages < <( + printf '%s\n' "${lifecycle_packages[@]}" | cut -d '|' -f 1 | sort -u +) + +missing_rpm=() +for package in "${packages[@]}"; do + mapfile -t artifacts < <(find "$package" -maxdepth 1 -type f -name '*.rpm' -print) + if [[ "${#artifacts[@]}" -ne 1 ]]; then + missing_rpm+=("$package") + fi +done +if [[ "${#missing_rpm[@]}" -gt 0 ]]; then + "${script_dir}/clean_build.sh" "${missing_rpm[@]}" +fi +"${script_dir}/verify_artifacts.sh" "${packages[@]}" + +prepare_stplr +build_images +if [[ "$deb_build_mode" == 'container' ]]; then + docker volume create "$deb_cache_volume" >/dev/null + import_stplr_source_cache \ + docker "$deb_image" "$deb_cache_volume" "$work_dir" "$source_cache" \ + "${packages[@]}" +else + echo '==> DEB packages build directly on the host runner' + prepare_host_deb_builder +fi + +deb_is_current() { + local package="$1" + local expected_name expected_version expected_release artifact + local -a existing + + mapfile -t existing < <(find "$package" -maxdepth 1 -type f -name '*.deb' -print) + [[ "${#existing[@]}" -eq 1 ]] || return 1 + artifact="${existing[0]}" + expected_name="$(stplr-spec get-field --path "${package}/Staplerfile" name)" + expected_version="$(stplr-spec get-field --path "${package}/Staplerfile" version)" + expected_release="$(stplr-spec get-field --path "${package}/Staplerfile" release)" + + [[ "$(dpkg-deb -f "$artifact" Package 2>/dev/null)" == "${expected_name}+stplr-default" ]] && + [[ "$(dpkg-deb -f "$artifact" Version 2>/dev/null)" == "${expected_version}-${expected_release}" ]] +} + +for package in "${packages[@]}"; do + if [[ "${NIVORA_REBUILD_DEB:-0}" != '1' ]] && deb_is_current "$package"; then + echo "==> DEB reuse ${package}" + continue + fi + echo "==> DEB build ${package}" + find "$package" -maxdepth 1 -type f -name '*.deb' -delete + if [[ "$deb_build_mode" == 'host' ]]; then + build_deb_on_host "$package" + else + docker run --rm --privileged \ + -e TERM=xterm-256color \ + -v "${repo_root}/${package}:/app" \ + -v "${deb_cache_volume}:/var/cache/stplr" \ + -w /app \ + "$deb_image" \ + stplr --interactive=false build --clean -s Staplerfile + fi +done + +build_previous_deb() { + local package="$1" + local system_name="$2" + local root="${work_dir}/previous-deb/${package}" + + install -d \ + "${root}/DEBIAN" \ + "${root}/usr/share/nivora-lifecycle-previous" + printf '%s\n' \ + "Package: ${system_name}" \ + 'Version: 0:0.0.2-1' \ + 'Architecture: all' \ + 'Maintainer: Nivora tests ' \ + "Provides: ${package}" \ + "Replaces: ${package}" \ + "Conflicts: ${package}" \ + 'Description: Previous Nivora package fixture for lifecycle tests' \ + >"${root}/DEBIAN/control" + printf 'previous fixture\n' \ + >"${root}/usr/share/nivora-lifecycle-previous/${package}" + dpkg-deb --root-owner-group --build "$root" \ + "${work_dir}/fixtures/previous-deb/${package}.deb" >/dev/null +} + +build_previous_rpm() { + local package="$1" + local system_name="$2" + local topdir="${work_dir}/previous-rpmbuild/${package}" + local spec="${topdir}/SPECS/previous.spec" + local built + + install -d \ + "${topdir}/BUILD" \ + "${topdir}/BUILDROOT" \ + "${topdir}/RPMS" \ + "${topdir}/SOURCES" \ + "${topdir}/SPECS" \ + "${topdir}/SRPMS" \ + "${topdir}/TMP" + printf '%s\n' \ + "Name: ${system_name}" \ + 'Version: 0.0.2' \ + 'Release: 1' \ + 'Summary: Previous Nivora package fixture for lifecycle tests' \ + 'Group: System/Configuration/Packaging' \ + 'License: MIT' \ + 'BuildArch: noarch' \ + "Provides: ${package}" \ + "Obsoletes: ${package}" \ + "Conflicts: ${package}" \ + '%description' \ + 'Previous Nivora package fixture for lifecycle tests.' \ + '%install' \ + 'mkdir -p %{buildroot}/usr/share/nivora-lifecycle-previous' \ + "printf 'previous fixture\\n' >%{buildroot}/usr/share/nivora-lifecycle-previous/${package}" \ + '%files' \ + "/usr/share/nivora-lifecycle-previous/${package}" \ + >"$spec" + if ! rpmbuild \ + --define "_topdir ${topdir}" \ + --define "_tmppath ${topdir}/TMP" \ + -bb "$spec" >"${topdir}/build.log" 2>&1; then + cat "${topdir}/build.log" >&2 + return 1 + fi + built="$(find "${topdir}/RPMS" -type f -name '*.rpm' -print -quit)" + [[ -n "$built" ]] + cp "$built" "${work_dir}/fixtures/previous-rpm/${package}.rpm" +} + +metadata_contains() { + local value="$1" + local expected="$2" + value="${value//|/,}" + value="${value// /}" + [[ ",${value}," == *",${expected},"* ]] +} + +printf '%s\n' "${lifecycle_packages[@]}" >"${work_dir}/lifecycle-packages.txt" + +while IFS='|' read -r package _; do + mapfile -t debs < <(find "$package" -maxdepth 1 -type f -name '*.deb' -print) + mapfile -t rpms < <(find "$package" -maxdepth 1 -type f -name '*.rpm' -print) + [[ "${#debs[@]}" -eq 1 && "${#rpms[@]}" -eq 1 ]] || { + echo "${package}: ожидалось по одному DEB и RPM" >&2 + exit 1 + } + build_previous_deb "$package" "$(dpkg-deb -f "${debs[0]}" Package)" + build_previous_rpm "$package" "$(rpm -qp --queryformat '%{NAME}' "${rpms[0]}")" + + for field in Provides Replaces Conflicts; do + value="$(dpkg-deb -f "${debs[0]}" "$field")" + metadata_contains "$value" "$package" || { + echo "${package}: DEB ${field} не содержит ${package}" >&2 + exit 1 + } + done + rpm -qp --provides "${rpms[0]}" | grep -Fxq "$package" + rpm -qp --obsoletes "${rpms[0]}" | grep -Fxq "$package" + rpm -qp --conflicts "${rpms[0]}" | grep -Fxq "$package" +done <"${work_dir}/lifecycle-packages.txt" + +cat >"${work_dir}/run-deb.sh" <<'EOF' +#!/bin/bash +set -euo pipefail + +export DEBIAN_FRONTEND=noninteractive +useradd --create-home nivora-test + +is_installed() { + [[ "$(dpkg-query -W -f='${db:Status-Abbrev}' "$1" 2>/dev/null || true)" == 'ii '* ]] +} + +while IFS='|' read -r package command_path state_path expected_a expected_b; do + mapfile -t artifacts < <(find "/repo/${package}" -maxdepth 1 -type f -name '*.deb' -print) + [[ "${#artifacts[@]}" -eq 1 ]] + artifact="${artifacts[0]}" + system_name="$(dpkg-deb -f "$artifact" Package)" + + echo "==> DEB lifecycle ${package}" + dpkg -i "/previous/${package}.deb" + install -d "${state_path%/*}" + printf 'keep\n' >"$state_path" + if [[ "$state_path" == /home/nivora-test/* ]]; then + chown -R nivora-test:nivora-test /home/nivora-test + fi + + apt-get -qq install -y "$artifact" + is_installed "$system_name" + test -x "$command_path" + [[ "$expected_a" == '-' ]] || test -e "$expected_a" + [[ "$expected_b" == '-' ]] || test -e "$expected_b" + + apt-get -qq remove -y "$system_name" + test -f "$state_path" + ! is_installed "$system_name" +done "${work_dir}/run-rpm.sh" <<'EOF' +#!/bin/bash +set -euo pipefail + +useradd --create-home nivora-test + +while IFS='|' read -r package command_path state_path expected_a expected_b; do + mapfile -t artifacts < <(find "/repo/${package}" -maxdepth 1 -type f -name '*.rpm' -print) + [[ "${#artifacts[@]}" -eq 1 ]] + artifact="${artifacts[0]}" + system_name="$(rpm -qp --queryformat '%{NAME}' "$artifact")" + + echo "==> RPM lifecycle ${package}" + rpm -ivh --quiet "/previous/${package}.rpm" + install -d "${state_path%/*}" + printf 'keep\n' >"$state_path" + if [[ "$state_path" == /home/nivora-test/* ]]; then + chown -R nivora-test:nivora-test /home/nivora-test + fi + + apt-get -qq install -y "$artifact" + rpm -q "$system_name" + test -x "$command_path" + [[ "$expected_a" == '-' ]] || test -e "$expected_a" + [[ "$expected_b" == '-' ]] || test -e "$expected_b" + + apt-get -qq remove -y "$system_name" + test -f "$state_path" + ! rpm -q "$system_name" +done str | None: + match = re.search( + rf"^{re.escape(field)}=(?:'([^']*)'|\"([^\"]*)\"|([^#\n]+))", + text, + re.MULTILINE, + ) + if not match: + return None + return next(value.strip() for value in match.groups() if value is not None) + + +def array(text: str, field: str) -> list[str] | None: + match = re.search( + rf"^{re.escape(field)}=\((.*?)\)", text, re.MULTILINE | re.DOTALL + ) + if not match: + return None + try: + return shlex.split(match.group(1), comments=True, posix=True) + except ValueError: + return None + + +def source_arrays(text: str) -> dict[str, list[str]]: + result: dict[str, list[str]] = {} + for match in re.finditer(r"^(sources(?:_[a-z0-9_]+)?)=\(", text, re.MULTILINE): + name = match.group(1) + values = array(text, name) + if values is not None: + result[name] = values + return result + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def local_source_name(source: str) -> str | None: + if not source.startswith("local:///"): + return None + value = unquote(source.removeprefix("local:///").split("?", 1)[0]) + path = Path(value) + if not value or path.is_absolute() or ".." in path.parts: + return "" + return value + + +def markdown_targets(text: str) -> set[str]: + return set(MARKDOWN_LINK_RE.findall(text)) | set(HTML_LINK_RE.findall(text)) + + +def validate_links(path: Path, errors: list[str]) -> None: + text = path.read_text(encoding="utf-8") + for target in markdown_targets(text): + parsed = urlsplit(target) + if parsed.scheme or target.startswith(("mailto:", "#")): + continue + local = unquote(parsed.path) + if not local: + continue + if local.startswith("/"): + errors.append(f"{path.relative_to(ROOT)}: unsafe local link: {target}") + continue + resolved = (path.parent / local).resolve() + try: + resolved.relative_to(ROOT) + except ValueError: + errors.append(f"{path.relative_to(ROOT)}: link escapes repository: {target}") + continue + if not resolved.exists(): + errors.append(f"{path.relative_to(ROOT)}: missing link target: {target}") + + +def validate_package(package: str, errors: list[str]) -> dict[str, object]: + directory = ROOT / package + staplerfile = directory / "Staplerfile" + text = staplerfile.read_text(encoding="utf-8") + + name = scalar(text, "name") + version = scalar(text, "version") + release = scalar(text, "release") + architectures = array(text, "architectures") + provides = array(text, "provides") + replaces = array(text, "replaces") + conflicts = array(text, "conflicts") + + if name != package: + errors.append(f"{package}: directory and name differ: {name!r}") + if not version: + errors.append(f"{package}: version is missing") + if not release or not release.isdigit() or int(release) < 1: + errors.append(f"{package}: release must be a positive integer") + if not architectures: + errors.append(f"{package}: architectures are missing") + elif any(item not in {"amd64", "arm64", "all"} for item in architectures): + errors.append(f"{package}: unsupported architecture value: {architectures}") + + if provides != [] or conflicts != []: + errors.append(f"{package}: provides/conflicts must not contain binary aliases") + if replaces != [package]: + errors.append(f"{package}: replaces must contain only its own base package name") + + if "package()" not in text or "files()" not in text: + errors.append(f"{package}: package() or files() is missing") + + arrays = source_arrays(text) + if not arrays: + errors.append(f"{package}: sources are missing") + for source_field, sources in arrays.items(): + checksum_field = source_field.replace("sources", "checksums", 1) + checksums = array(text, checksum_field) + if checksums is None: + errors.append(f"{package}: {checksum_field} is missing") + continue + if len(sources) != len(checksums): + errors.append( + f"{package}: {source_field}/{checksum_field} lengths differ " + f"({len(sources)} != {len(checksums)})" + ) + continue + for source, checksum in zip(sources, checksums, strict=True): + if checksum == "SKIP" or not CHECKSUM_RE.fullmatch(checksum): + errors.append(f"{package}: invalid checksum for {source}: {checksum}") + continue + if source.startswith("http://"): + errors.append(f"{package}: insecure source URL: {source}") + if source.startswith("git+") and "#" not in source: + errors.append(f"{package}: unpinned Git source: {source}") + + local_name = local_source_name(source) + if local_name is None: + continue + if local_name == "": + errors.append(f"{package}: unsafe local source: {source}") + continue + local_path = directory / local_name + if not local_path.is_file(): + errors.append(f"{package}: missing local source: {local_name}") + continue + expected = checksum.removeprefix("sha256:") + actual = sha256(local_path) + if actual != expected: + errors.append( + f"{package}: checksum mismatch for {local_name}: {actual} != {expected}" + ) + + for hook in re.findall(r"\['[^']+'\]='([^']+)'", text): + hook_path = directory / hook + if not hook_path.is_file(): + errors.append(f"{package}: missing lifecycle script: {hook}") + elif not os.access(hook_path, os.X_OK): + errors.append(f"{package}: lifecycle script is not executable: {hook}") + + update_check = directory / ".stapler/update-check" + if not update_check.is_file() or not os.access(update_check, os.X_OK): + errors.append(f"{package}: executable .stapler/update-check is required") + + return { + "name": name, + "version": version, + "architectures": architectures or [], + } + + +def validate_readme(metadata: dict[str, dict[str, object]], errors: list[str]) -> None: + path = ROOT / "README.md" + text = path.read_text(encoding="utf-8") + count_match = re.search(r"\s*\*\*(\d+) пакет", text) + if not count_match or int(count_match.group(1)) != len(EXPECTED_PACKAGES): + errors.append("README.md: package counter is stale") + if text.count("### ") != 6: + errors.append("README.md: catalog must contain exactly six categories") + + for package, values in metadata.items(): + command = f"stplr install nivora/{package}" + catalog_rows = [ + line for line in text.splitlines() if line.startswith("|") and command in line + ] + if len(catalog_rows) != 1: + errors.append(f"README.md: expected one catalog command for {package}") + version = str(values["version"]) + if f"`{version}`" not in text: + errors.append(f"README.md: version {version} is missing for {package}") + + +def validate_repository_text(errors: list[str]) -> None: + for path in ROOT.rglob("*"): + if not path.is_file() or ".git" in path.parts: + continue + relative = path.relative_to(ROOT) + if path.suffix.lower() in {".png", ".ico", ".zip", ".gz"}: + continue + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + + scans_validator_source = relative == Path("tools/validate_repo.py") + if not scans_validator_source and ( + "/home/cheviiot" in text or "/.codex/attachments/" in text + ): + errors.append(f"{relative}: personal path is forbidden") + if not scans_validator_source and "chmod 777" in text: + errors.append(f"{relative}: chmod 777 is forbidden") + if not scans_validator_source and re.search(r"\brm\s+-rf\b", text): + allowed_purge = relative in { + Path("tailscale/tailscale-purge-data"), + Path("netbird/netbird-purge-data"), + } + if not allowed_purge or "--yes" not in text: + errors.append(f"{relative}: unsafe rm -rf") + for pattern in SECRET_PATTERNS: + if pattern.search(text): + errors.append(f"{relative}: possible secret detected") + + if text.startswith("#!") and not os.access(path, os.X_OK): + errors.append(f"{relative}: script is not executable") + + +def main() -> int: + errors: list[str] = [] + + for required in sorted(REQUIRED_ROOT_FILES): + if not (ROOT / required).is_file(): + errors.append(f"missing required file: {required}") + + package_dirs = tuple( + sorted(path.name for path in ROOT.iterdir() if (path / "Staplerfile").is_file()) + ) + if package_dirs != EXPECTED_PACKAGES: + errors.append( + "package list mismatch: " + f"expected {', '.join(EXPECTED_PACKAGES)}; got {', '.join(package_dirs)}" + ) + + metadata: dict[str, dict[str, object]] = {} + for package in package_dirs: + metadata[package] = validate_package(package, errors) + + validate_readme(metadata, errors) + for path in sorted([*ROOT.glob("*.md"), *ROOT.glob("docs/**/*.md")]): + validate_links(path, errors) + validate_repository_text(errors) + + if errors: + print("Nivora validation failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + + print(f"OK: validated {len(package_dirs)} Nivora packages") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/verify_artifacts.sh b/tools/verify_artifacts.sh new file mode 100755 index 0000000..ba73cbe --- /dev/null +++ b/tools/verify_artifacts.sh @@ -0,0 +1,330 @@ +#!/bin/bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +cd "$repo_root" + +mapfile -t all_packages < <( + for staplerfile in */Staplerfile; do + dirname "$staplerfile" + done | sort +) + +packages=() +if [[ "${1:-}" == '--all' ]]; then + packages=("${all_packages[@]}") + shift +else + packages=("$@") +fi + +[[ "${#packages[@]}" -gt 0 ]] || { + echo 'Использование: tools/verify_artifacts.sh {--all|package...}' >&2 + exit 2 +} + +command -v rpm >/dev/null 2>&1 || { + echo 'Для проверки RPM требуется команда rpm' >&2 + exit 2 +} +command -v stplr-spec >/dev/null 2>&1 || { + echo 'Для проверки метаданных требуется stplr-spec' >&2 + exit 2 +} + +contains_path() { + local pattern="$1" + local path + for path in "${payload[@]}"; do + # shellcheck disable=SC2053 + [[ "$path" == $pattern ]] && return 0 + done + return 1 +} + +for package in "${packages[@]}"; do + [[ -f "${package}/Staplerfile" ]] || { + echo "Неизвестный пакет: ${package}" >&2 + exit 2 + } + + mapfile -t artifacts < <(find "$package" -maxdepth 1 -type f -name '*.rpm' -print) + [[ "${#artifacts[@]}" -eq 1 ]] || { + echo "${package}: ожидался один RPM, найдено ${#artifacts[@]}" >&2 + exit 1 + } + + artifact="${artifacts[0]}" + package_name="$(stplr-spec get-field --path "${package}/Staplerfile" name)" + rpm_name="$(rpm -qp --queryformat '%{NAME}' "$artifact")" + [[ "$rpm_name" == "${package_name}+stplr-"* ]] || { + echo "${package}: неверное имя RPM: ${rpm_name}" >&2 + exit 1 + } + + mapfile -t payload < <(rpm -qlp "$artifact") + contains_path "/usr/share/licenses/${package_name}/*" || { + echo "${package}: отсутствует лицензия в собственном namespace" >&2 + exit 1 + } + contains_path '/usr/share/licenses/LICENSE' && { + echo "${package}: обнаружен общий конфликтный путь лицензии" >&2 + exit 1 + } + + recipe="$(<"${package}/Staplerfile")" + if [[ "$recipe" == *'files-find-binary'* ]]; then + contains_path '/usr/bin/*' || contains_path '/usr/sbin/*' || { + echo "${package}: files-find-binary не добавил исполняемые файлы" >&2 + exit 1 + } + fi + if [[ "$recipe" == *'files-find-desktop'* ]]; then + contains_path '/usr/share/applications/*.desktop' || { + echo "${package}: files-find-desktop не добавил desktop-файл" >&2 + exit 1 + } + fi + if [[ "$recipe" == *'files-find-systemd'* ]]; then + contains_path '/usr/lib/systemd/system/*' || { + echo "${package}: files-find-systemd не добавил unit-файл" >&2 + exit 1 + } + fi + + if [[ "$package" == 'claude-desktop' ]]; then + for required_path in \ + /usr/bin/claude-alt \ + /usr/bin/claude-desktop-account2 \ + /usr/lib/claude-desktop/resources/TrayIconLinux.png \ + /usr/lib/claude-desktop/resources/TrayIconLinux-Dark.png \ + /usr/lib/claude-alt/claude-alt-bin \ + /usr/lib/claude-alt/resources/app.asar \ + /usr/lib/claude-alt/resources/icon.png \ + /usr/lib/claude-alt/resources/TrayIconLinux.png \ + /usr/lib/claude-alt/resources/TrayIconLinux-Dark.png \ + /usr/share/applications/com.anthropic.ClaudeAlt.desktop \ + /usr/share/icons/hicolor/512x512/apps/claude-alt.png; do + contains_path "$required_path" || { + echo "${package}: отсутствует компонент ClaudeAlt: ${required_path}" >&2 + exit 1 + } + done + contains_path '/usr/share/applications/claude-desktop-account2.desktop' && { + echo "${package}: обнаружен устаревший desktop-файл второго профиля" >&2 + exit 1 + } + fi + + if [[ "$package" == 'balena-etcher' ]]; then + for required_path in \ + /usr/bin/balena-etcher \ + /usr/lib/balena-etcher/balena-etcher \ + /usr/lib/balena-etcher/balenaEtcher \ + /usr/lib/balena-etcher/resources/etcher-util \ + /usr/share/applications/balena-etcher.desktop \ + /usr/share/pixmaps/balena-etcher.png; do + contains_path "$required_path" || { + echo "${package}: отсутствует компонент balenaEtcher: ${required_path}" >&2 + exit 1 + } + done + + balena_command_target="$( + rpm -qp --dump "$artifact" | + awk '$1 == "/usr/bin/balena-etcher" {print $11}' + )" + balena_alias_target="$( + rpm -qp --dump "$artifact" | + awk '$1 == "/usr/lib/balena-etcher/balenaEtcher" {print $11}' + )" + [[ "$balena_command_target" == '../lib/balena-etcher/balena-etcher' ]] || { + echo "${package}: неверная ссылка /usr/bin/balena-etcher" >&2 + exit 1 + } + [[ "$balena_alias_target" == 'balena-etcher' ]] || { + echo "${package}: обнаружена битая upstream-ссылка balenaEtcher" >&2 + exit 1 + } + fi + + if [[ "$package" == 'codex' ]]; then + for required_path in \ + /usr/bin/codex-app \ + /usr/bin/codex-computer-use-linux \ + /opt/codex-app/resources/app.asar \ + /opt/codex-app/resources/codex.asar \ + /opt/codex-app/resources/plugins/openai-bundled/.agents/plugins/marketplace.json \ + /opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/.codex-plugin/plugin.json \ + /opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/.mcp.json \ + /opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/assets/app-icon.png \ + /opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/bin/codex-computer-use-linux \ + /opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/bin/codex-computer-use-cosmic \ + /opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/bin/computer-use-linux-cosmic \ + /usr/share/applications/codex-app.desktop \ + /usr/share/icons/hicolor/512x512/apps/codex-app.png; do + contains_path "$required_path" || { + echo "${package}: отсутствует upstream-компонент Codex: ${required_path}" >&2 + exit 1 + } + done + + codex_command_target="$( + rpm -qp --dump "$artifact" | + awk '$1 == "/usr/bin/codex-app" {print $11}' + )" + [[ "$codex_command_target" == '/opt/codex-app/codex-app' ]] || { + echo "${package}: команда Codex запускается не напрямую: ${codex_command_target}" >&2 + exit 1 + } + codex_computer_use_target="$( + rpm -qp --dump "$artifact" | + awk '$1 == "/usr/bin/codex-computer-use-linux" {print $11}' + )" + [[ "$codex_computer_use_target" == '/opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/bin/codex-computer-use-linux' ]] || { + echo "${package}: неверная ссылка Computer Use: ${codex_computer_use_target}" >&2 + exit 1 + } + fi + + if [[ "$package" == 'github-desktop' ]]; then + for required_path in \ + /usr/bin/github-desktop \ + /opt/github-desktop/desktop \ + /opt/github-desktop/resources/app \ + /usr/share/applications/github-desktop.desktop \ + /usr/share/icons/hicolor/scalable/apps/github-desktop.svg; do + contains_path "$required_path" || { + echo "${package}: отсутствует компонент GitHub Desktop: ${required_path}" >&2 + exit 1 + } + done + + github_desktop_target="$( + rpm -qp --dump "$artifact" | + awk '$1 == "/usr/bin/github-desktop" {print $11}' + )" + [[ "$github_desktop_target" == '/opt/github-desktop/desktop' ]] || { + echo "${package}: команда GitHub Desktop запускается не напрямую" >&2 + exit 1 + } + fi + + if [[ "$package" == 'opencode' ]]; then + for required_path in \ + /usr/bin/opencode-desktop \ + /opt/OpenCode/ai.opencode.desktop \ + /opt/OpenCode/resources/app.asar \ + /usr/share/applications/opencode-desktop.desktop \ + /usr/share/icons/hicolor/128x128/apps/ai.opencode.desktop.png; do + contains_path "$required_path" || { + echo "${package}: отсутствует upstream-компонент OpenCode: ${required_path}" >&2 + exit 1 + } + done + + opencode_command_target="$( + rpm -qp --dump "$artifact" | + awk '$1 == "/usr/bin/opencode-desktop" {print $11}' + )" + [[ "$opencode_command_target" == '/opt/OpenCode/ai.opencode.desktop' ]] || { + echo "${package}: команда OpenCode запускается не напрямую: ${opencode_command_target}" >&2 + exit 1 + } + contains_path '/usr/lib/opencode-desktop/*' && { + echo "${package}: обнаружен удалённый wrapper OpenCode" >&2 + exit 1 + } + fi + + if [[ "$package" == 'ventoy' ]]; then + for required_path in \ + /usr/bin/ventoy \ + '/opt/ventoy/VentoyGUI.*' \ + /opt/ventoy/boot/boot.img \ + /opt/ventoy/tool/VentoyWorker.sh \ + /opt/ventoy/ventoy/ventoy.disk.img.xz \ + /usr/share/applications/ventoy.desktop \ + /usr/share/icons/hicolor/128x128/apps/ventoy.png; do + contains_path "$required_path" || { + echo "${package}: отсутствует компонент Ventoy: ${required_path}" >&2 + exit 1 + } + done + fi + + if [[ "$package" == 'yandex-browser-stable' ]]; then + for command in cpio rpm2cpio; do + command -v "$command" >/dev/null 2>&1 || { + echo "${package}: для проверки desktop-файлов требуется ${command}" >&2 + exit 2 + } + done + for required_path in \ + /usr/bin/yandex-browser \ + /usr/bin/yandex-browser-stable \ + /opt/yandex/browser/yandex-browser \ + /opt/yandex/browser/yandex_browser \ + /opt/yandex/browser/yandex_browser-sandbox \ + /usr/share/appdata/yandex-browser.appdata.xml \ + /usr/share/applications/ru.yandex.desktop.browser.desktop \ + /usr/share/applications/yandex-browser.desktop \ + /usr/share/icons/hicolor/256x256/apps/yandex-browser.png \ + /usr/share/mime/packages/yandex-browser-yprotect.xml; do + contains_path "$required_path" || { + echo "${package}: отсутствует компонент Яндекс Браузера: ${required_path}" >&2 + exit 1 + } + done + contains_path '/etc/cron.daily/yandex-browser' && { + echo "${package}: обнаружена upstream cron-задача обновления" >&2 + exit 1 + } + contains_path '/etc/xdg/autostart/yandex-browser_user_setup.desktop' && { + echo "${package}: обнаружен нежелательный upstream autostart" >&2 + exit 1 + } + + compatibility_desktop="$( + set +o pipefail + rpm2cpio "$artifact" | + cpio -i --quiet --to-stdout \ + /usr/share/applications/yandex-browser.desktop + )" + canonical_desktop="$( + set +o pipefail + rpm2cpio "$artifact" | + cpio -i --quiet --to-stdout \ + /usr/share/applications/ru.yandex.desktop.browser.desktop + )" + desktop_entry_hidden() { + awk ' + $0 == "[Desktop Entry]" { in_entry = 1; next } + /^\[/ { in_entry = 0 } + in_entry && $0 == "NoDisplay=true" { found = 1 } + END { exit !found } + ' + } + desktop_entry_hidden <<<"$compatibility_desktop" || { + echo "${package}: совместимый desktop-id виден в меню приложений" >&2 + exit 1 + } + if desktop_entry_hidden <<<"$canonical_desktop"; then + echo "${package}: canonical desktop-id ошибочно скрыт" >&2 + exit 1 + fi + fi + + while read -r path _ _ _ mode _; do + [[ "$path" == /usr/bin/* || "$path" == /usr/sbin/* ]] || continue + [[ "$mode" == 0100755 || "$mode" == 0120000 ]] || { + echo "${package}: неверные права ${mode} у ${path}" >&2 + exit 1 + } + done < <(rpm -qp --dump "$artifact") + + echo "OK: ${package} ($(basename "$artifact"), ${#payload[@]} путей)" +done + +echo "OK: payload проверен для ${#packages[@]} пакетов" diff --git a/ventoy/.stapler/update-check b/ventoy/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/ventoy/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/ventoy/Staplerfile b/ventoy/Staplerfile new file mode 100644 index 0000000..62a0a88 --- /dev/null +++ b/ventoy/Staplerfile @@ -0,0 +1,141 @@ +name='ventoy' +version=1.1.17 +release=1 +summary='Create and manage multiboot USB drives' +summary_ru='Создание и управление мультизагрузочными USB-накопителями' +group='System/Configuration/Hardware' +desc='Ventoy is an open source tool for creating multiboot USB drives for ISO, WIM, IMG, VHD(x) and EFI files. This package includes the official native Linux GUI.' +desc_ru='Ventoy — открытая утилита для создания мультизагрузочных USB-накопителей с файлами ISO, WIM, IMG, VHD(x) и EFI. Пакет включает официальный нативный GUI для Linux.' +homepage='https://www.ventoy.net/' +maintainer='Ventoy Project ' + +architectures=('amd64' 'arm64') + +license=('GPL-3.0-only') + +provides=() +replaces=('ventoy') +conflicts=() + +# В официальном архиве находятся готовые нативные программы и низкоуровневые +# инструменты для работы с дисками. +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=( + 'libgtk-3-0' 'policykit-1' 'parted' 'fdisk' 'dosfstools' + 'dmsetup' 'udev' 'xz-utils' +) +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=( + 'gtk3' 'polkit' 'parted' 'util-linux' 'dosfstools' + 'device-mapper' 'systemd-udev' 'xz' +) +deps_arch=( + 'gtk3' 'polkit' 'parted' 'util-linux' 'dosfstools' + 'device-mapper' 'systemd' 'xz' +) +deps_opensuse=( + 'gtk3' 'polkit' 'parted' 'util-linux' 'dosfstools' + 'device-mapper' 'systemd-udev' 'xz' +) +deps_altlinux=( + 'libgtk+3' 'polkit' 'parted' 'fdisk' 'dosfstools' + 'dmsetup' 'udev' 'xz' +) + +opt_deps=() +opt_deps_debian=('libqt5widgets5') +opt_deps_ubuntu=("${opt_deps_debian[@]}") +opt_deps_fedora=('qt5-qtbase-gui') +opt_deps_arch=('qt5-base') +opt_deps_opensuse=('libQt5Widgets5') +opt_deps_altlinux=('libqt5-widgets') + +sources=( + "https://github.com/ventoy/Ventoy/releases/download/v${version}/ventoy-${version}-linux.tar.gz?~archive=false&~name=ventoy.tar.gz" + "https://raw.githubusercontent.com/ventoy/Ventoy/v${version}/COPYING?~archive=false&~name=COPYING" + 'local:///ventoy' + 'local:///ventoy.desktop' +) + +sources_arm64=( + "https://github.com/ventoy/Ventoy/releases/download/v${version}/ventoy-${version}-linux.tar.gz?~archive=false&~name=ventoy.tar.gz" + "https://raw.githubusercontent.com/ventoy/Ventoy/v${version}/COPYING?~archive=false&~name=COPYING" + 'local:///ventoy' + 'local:///ventoy.desktop' +) + +checksums=( + 'sha256:7fb4ed08cef6a6b4d39dd19260d8c80291a78dfdf9af7d461571e23cbbc43805' + 'sha256:3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986' + 'sha256:8f0a81d29a973844791d3fb5dd0f572684855b6a66edcc48f9d39f4e77f72de2' + 'sha256:98c00e3911109644ee139e2194ea00531f847d89289ddbd062566f28a34823c1' +) + +checksums_arm64=( + 'sha256:7fb4ed08cef6a6b4d39dd19260d8c80291a78dfdf9af7d461571e23cbbc43805' + 'sha256:3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986' + 'sha256:8f0a81d29a973844791d3fb5dd0f572684855b6a66edcc48f9d39f4e77f72de2' + 'sha256:98c00e3911109644ee139e2194ea00531f847d89289ddbd062566f28a34823c1' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + tar -xzf ventoy.tar.gz + source_root="${srcdir}/ventoy-${version}" + + case "$(uname -m)" in + x86_64 | amd64) + upstream_arch='x86_64' + ;; + aarch64 | arm64) + upstream_arch='aarch64' + ;; + *) + echo "Неподдерживаемая архитектура сборки: $(uname -m)" >&2 + return 1 + ;; + esac + + install -dm755 "${pkgdir}/opt/ventoy" + cp -a "${source_root}/." "${pkgdir}/opt/ventoy/" + + for candidate in x86_64 aarch64 i386 mips64el; do + if [[ "$candidate" == "$upstream_arch" ]]; then + continue + fi + + find "${pkgdir}/opt/ventoy/VentoyGUI.${candidate}" \ + -maxdepth 0 -type f -delete 2>/dev/null || true + if [[ -d "${pkgdir}/opt/ventoy/tool/${candidate}" ]]; then + find "${pkgdir}/opt/ventoy/tool/${candidate}" -mindepth 1 -delete + rmdir "${pkgdir}/opt/ventoy/tool/${candidate}" + fi + done + + install-binary ventoy + install-desktop ventoy.desktop + install -Dm644 \ + "${source_root}/WebUI/static/img/VentoyLogo.png" \ + "${pkgdir}/usr/share/icons/hicolor/128x128/apps/ventoy.png" + install-license COPYING "ventoy/COPYING" + install-license "${source_root}/README" "ventoy/README" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/opt/ventoy/**/*" \ + "/usr/share/icons/hicolor/128x128/apps/ventoy.png" +} diff --git a/ventoy/postinstall.sh b/ventoy/postinstall.sh new file mode 100755 index 0000000..74f65b4 --- /dev/null +++ b/ventoy/postinstall.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" \ + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/ventoy/postremove.sh b/ventoy/postremove.sh new file mode 100755 index 0000000..74f65b4 --- /dev/null +++ b/ventoy/postremove.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" \ + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/ventoy/stapler-repo.toml b/ventoy/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/ventoy/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml" diff --git a/ventoy/ventoy b/ventoy/ventoy new file mode 100755 index 0000000..8c9a9bd --- /dev/null +++ b/ventoy/ventoy @@ -0,0 +1,25 @@ +#!/bin/bash +set -euo pipefail + +install_dir="${VENTOY_INSTALL_DIR:-/opt/ventoy}" + +case "$(uname -m)" in +x86_64 | amd64) + gui='VentoyGUI.x86_64' + ;; +aarch64 | arm64) + gui='VentoyGUI.aarch64' + ;; +*) + echo "Ventoy: неподдерживаемая архитектура $(uname -m)" >&2 + exit 1 + ;; +esac + +if [[ ! -x "${install_dir}/${gui}" ]]; then + echo "Ventoy: не найден исполняемый файл ${install_dir}/${gui}" >&2 + exit 1 +fi + +cd "$install_dir" +exec "./${gui}" "$@" diff --git a/ventoy/ventoy.desktop b/ventoy/ventoy.desktop new file mode 100644 index 0000000..dd11fbf --- /dev/null +++ b/ventoy/ventoy.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Name=Ventoy +Comment=Create and manage multiboot USB drives +Comment[ru]=Создание и управление мультизагрузочными USB-накопителями +Exec=ventoy +Icon=ventoy +Terminal=false +Categories=System;Utility; +Keywords=USB;ISO;boot;multiboot;flash; +StartupNotify=true diff --git a/vual/.stapler/update-check b/vual/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/vual/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/vual/LICENSE b/vual/LICENSE new file mode 100644 index 0000000..6d880bf --- /dev/null +++ b/vual/LICENSE @@ -0,0 +1,14 @@ +Сведения о лицензии пакета Vual + +Лицензия: GPL-3.0-or-later. +Исходный проект: https://github.com/Cheviiot/Vual + +Нюансы пакета: +- Пакет устанавливает GTK4/libadwaita-приложение Vual для запуска Cheat Engine в Steam-играх через Proton. +- Код Vual распространяется на условиях GNU GPL версии 3 или любой более поздней версии. +- Встроенная загрузка Cheat Engine не означает, что Cheat Engine входит в этот пакет; скачиваемые сторонние компоненты регулируются собственными лицензиями. +- Steam, Proton, игры и игровые файлы в пакет не входят и регулируются собственными условиями. +- При распространении измененных версий нужно сохранять условия GPL и предоставлять соответствующий исходный код. + +Полный официальный текст GPL-3.0: +https://www.gnu.org/licenses/gpl-3.0.txt diff --git a/vual/Staplerfile b/vual/Staplerfile new file mode 100644 index 0000000..a24c2c8 --- /dev/null +++ b/vual/Staplerfile @@ -0,0 +1,136 @@ +name='vual' +version='0.3.1' +release=5 +summary='Launch Cheat Engine for Steam games through Proton' +summary_ru='Запуск Cheat Engine для Steam-игр через Proton' +group='Games' +desc='Vual is a GTK4/libadwaita application for launching Cheat Engine in Steam games +through Proton. Displays Steam library with cover art, one-click LaunchOptions setup +for protonhax, and built-in Cheat Engine download and installation.' +desc_ru='Vual — GTK4/libadwaita-приложение для запуска Cheat Engine в Steam-играх +через Proton. Отображает библиотеку Steam с обложками, настройка LaunchOptions +для protonhax одним кликом, встроенная загрузка и установка Cheat Engine.' +homepage='https://github.com/Cheviiot/Vual' +maintainer='Cheviiot ' + +architectures=('all') + +license=('GPL-3.0-or-later') + +provides=() +replaces=('vual') +conflicts=() + +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=( + 'python3' + 'gtk4' + 'libadwaita' +) +deps_debian=( + 'python3' + 'python3-gi' + 'python3-requests' + 'gir1.2-gtk-4.0' + 'gir1.2-adw-1' +) +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=( + 'python3' + 'python3-gobject' + 'python3-requests' + 'gtk4' + 'libadwaita' +) +deps_arch=( + 'python' + 'python-gobject' + 'python-requests' + 'gtk4' + 'libadwaita' +) +deps_altlinux=( + 'python3' + 'python3-module-pygobject3' + 'python3-module-requests' + 'libgtk4' + 'libadwaita' +) +deps_opensuse=( + 'python3' + 'python3-gobject' + 'python3-requests' + 'gtk4' + 'libadwaita' +) + +sources=( + "https://github.com/Cheviiot/Vual/archive/refs/tags/v${version}.tar.gz" + 'local:///LICENSE' +) +checksums=( + '84135424da73aa1e6b6b0ae7146527bf07c9f1ed22d5b78954a190c066195104' + '8a79d2011a095671073f0a44096ee200d3ef101979fb039034c6e5f5c9d7156b' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +appstream_app_id='io.github.vual' + +package() { + # GitHub создаёт архив с именем Vual- + src="${srcdir}/Vual-${version}" + + # Python модуль + install -dm755 "${pkgdir}/usr/lib/vual" + cp -r "${src}/src/vual" "${pkgdir}/usr/lib/vual/" + find "${pkgdir}/usr/lib/vual" -depth -path '*/__pycache__*' -delete + + # Переводы + if [[ -d "${src}/po" ]]; then + cp -r "${src}/po" "${pkgdir}/usr/lib/vual/" + fi + + # Лаунчер + install -Dm755 "${src}/vual" "${pkgdir}/usr/bin/vual" + + # Обновляем путь в лаунчере: указываем на установленное расположение + sed -i 's|sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))|sys.path.insert(0, "/usr/lib/vual")|' \ + "${pkgdir}/usr/bin/vual" + sed -i '/pycache_prefix/d' "${pkgdir}/usr/bin/vual" + + # Desktop файл + install-desktop "${src}/data/io.github.vual.desktop" + sed -i 's/^Categories=.*/Categories=Game;/' \ + "${pkgdir}/usr/share/applications/io.github.vual.desktop" + + # Иконки всех размеров + for size in 16 24 32 48 64 128 256 512; do + install -Dm644 "${src}/data/icons/hicolor/${size}x${size}/apps/io.github.vual.png" \ + "${pkgdir}/usr/share/icons/hicolor/${size}x${size}/apps/io.github.vual.png" + done + + # AppStream metainfo + install -Dm644 "${src}/data/io.github.vual.metainfo.xml" \ + "${pkgdir}/usr/share/metainfo/io.github.vual.metainfo.xml" + + # Лицензия + install-license "${src}/LICENSE" "vual/LICENSE.upstream" + install-license "${srcdir}/LICENSE" "vual/LICENSE.nivora" +} + +files() { + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/usr/lib/vual/**/*" \ + "/usr/share/icons/hicolor/*/apps/io.github.vual.png" \ + "/usr/share/metainfo/io.github.vual.metainfo.xml" +} diff --git a/vual/postinstall.sh b/vual/postinstall.sh new file mode 100755 index 0000000..3dab81c --- /dev/null +++ b/vual/postinstall.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +# Обновляем кэш иконок +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +# Обновляем desktop базу данных +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi diff --git a/vual/postremove.sh b/vual/postremove.sh new file mode 100755 index 0000000..3dab81c --- /dev/null +++ b/vual/postremove.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +# Обновляем кэш иконок +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi + +# Обновляем desktop базу данных +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi diff --git a/vual/stapler-repo.toml b/vual/stapler-repo.toml new file mode 100644 index 0000000..d35f9bf --- /dev/null +++ b/vual/stapler-repo.toml @@ -0,0 +1 @@ +[inherit] diff --git a/yandex-browser-stable/.stapler/update-check b/yandex-browser-stable/.stapler/update-check new file mode 100755 index 0000000..efb050a --- /dev/null +++ b/yandex-browser-stable/.stapler/update-check @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "${package_dir}/.." && pwd)" +exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")" diff --git a/yandex-browser-stable/LICENSE b/yandex-browser-stable/LICENSE new file mode 100644 index 0000000..576b3f2 --- /dev/null +++ b/yandex-browser-stable/LICENSE @@ -0,0 +1,17 @@ +Сведения о лицензии пакета Яндекс Браузера + +Лицензия: Custom. +Официальный сайт: https://browser.yandex.ru/ +Условия лицензии: https://yandex.ru/legal/browser_agreement/ +Источник Linux-пакета: https://repo.yandex.ru/yandex-browser/deb/ + +Нюансы пакета: +- Пакет использует официальный неизменённый deb-артефакт Яндекс Браузера для Linux. +- Яндекс Браузер является проприетарным приложением. +- Лицензия разрешает бесплатное использование, а также распространение программы + в неизменённом виде исключительно в некоммерческих целях. +- Использование браузера, синхронизации, Алисы и других сетевых сервисов + регулируется условиями Яндекса. +- Рецепт Nivora не подключает системный репозиторий Яндекса и не устанавливает + cron-задачу автообновления: обновления выполняются через Stapler. +- Пакет помечен как несвободный через `nonfree=1`. diff --git a/yandex-browser-stable/Staplerfile b/yandex-browser-stable/Staplerfile new file mode 100644 index 0000000..55c9cfe --- /dev/null +++ b/yandex-browser-stable/Staplerfile @@ -0,0 +1,149 @@ +name='yandex-browser-stable' +version='26.4.1.1110' +release=3 +summary='Fast and secure Chromium-based web browser from Yandex' +summary_ru='Быстрый и безопасный браузер Яндекса на основе Chromium' +group='Networking/WWW' +desc='Yandex Browser combines Chromium technologies with built-in page translation, security protection, synchronization and Yandex services.' +desc_ru='Яндекс Браузер объединяет технологии Chromium со встроенным переводом страниц, защитой, синхронизацией и сервисами Яндекса.' +homepage='https://browser.yandex.ru/' +maintainer='Yandex Browser Team ' + +architectures=('amd64') + +license=('Custom') +nonfree=1 +nonfree_msg='Яндекс Браузер является проприетарным приложением. Использование браузера, синхронизации и сетевых сервисов регулируется лицензионным соглашением Яндекса.' +nonfree_url='https://yandex.ru/legal/browser_agreement/' + +provides=() +replaces=('yandex-browser-stable') +conflicts=() + +# Chromium и вспомогательные компоненты поставляются в официальном DEB. +auto_reqprov_method="dirty" +auto_req=0 +auto_prov=0 + +deps=() +deps_debian=( + 'binutils' 'ca-certificates' 'fonts-liberation' 'jq' + 'libasound2' 'libatk-bridge2.0-0' 'libatk1.0-0' 'libatspi2.0-0' + 'libc6' 'libcairo2' 'libcups2' 'libcurl4' 'libdbus-1-3' 'libexpat1' + 'libgbm1' 'libglib2.0-0' 'libgtk-3-0 | libgtk-4-1' 'libnspr4' + 'libnss3' 'libpango-1.0-0' 'libudev1' 'libvulkan1' 'libx11-6' + 'libxcb1' 'libxcomposite1' 'libxdamage1' 'libxext6' 'libxfixes3' + 'libxkbcommon0' 'libxrandr2' 'squashfs-tools' 'wget' 'xdg-utils' +) +deps_ubuntu=("${deps_debian[@]}") +deps_fedora=( + 'binutils' 'ca-certificates' 'liberation-fonts' 'jq' + 'alsa-lib' 'at-spi2-atk' 'at-spi2-core' 'atk' 'glibc' 'cairo' + 'cups-libs' 'libcurl' 'dbus-libs' 'expat' 'mesa-libgbm' 'glib2' + 'gtk3' 'nspr' 'nss' 'pango' 'systemd-libs' 'vulkan-loader' + 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' + 'libxkbcommon' 'libXrandr' 'squashfs-tools' 'wget' 'xdg-utils' +) +deps_arch=( + 'binutils' 'ca-certificates' 'ttf-liberation' 'jq' + 'alsa-lib' 'at-spi2-core' 'glibc' 'cairo' 'cups' 'curl' 'dbus' + 'expat' 'mesa' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'systemd-libs' + 'vulkan-icd-loader' 'libx11' 'libxcb' 'libxcomposite' 'libxdamage' + 'libxext' 'libxfixes' 'libxkbcommon' 'libxrandr' 'squashfs-tools' + 'wget' 'xdg-utils' +) +deps_opensuse=( + 'binutils' 'ca-certificates' 'liberation-fonts' 'jq' + 'libasound2' 'libatk-1_0-0' 'libatk-bridge-2_0-0' 'at-spi2-core' + 'glibc' 'libcairo2' 'libcups2' 'libcurl4' 'libdbus-1-3' 'libexpat1' + 'libgbm1' 'glib2' 'libgtk-3-0' 'mozilla-nspr' 'mozilla-nss' + 'libpango-1_0-0' 'libudev1' 'libvulkan1' 'libX11-6' 'libxcb1' + 'libXcomposite1' 'libXdamage1' 'libXext6' 'libXfixes3' + 'libxkbcommon0' 'libXrandr2' 'squashfs' 'wget' 'xdg-utils' +) +deps_altlinux=( + 'binutils' 'ca-certificates' 'fonts-ttf-liberation' 'jq' + 'libalsa' 'at-spi2-atk' 'libatk' 'libat-spi2-core' 'glibc' + 'libcairo' 'libcups' 'libcurl' 'libdbus' 'libexpat' 'libgbm' + 'glib2' 'libgtk+3' 'libnspr' 'libnss' 'libpango' 'libudev1' + 'libvulkan1' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' + 'libXext' 'libXfixes' 'libxkbcommon' 'libXrandr' 'squashfs-tools' + 'wget' 'xdg-utils' +) + +build_deps=('binutils') + +sources=( + "https://repo.yandex.ru/yandex-browser/deb/pool/main/y/yandex-browser-stable/yandex-browser-stable_${version}-1_amd64.deb?~archive=false&~name=yandex-browser.deb" + 'local:///LICENSE' +) + +checksums=( + 'sha256:27c8570c68a5e1b475304aaa4e8accefbc21e150b644a9cbc998ed32d952c8ba' + 'sha256:36fb82bc8a2bb99eea41807712d9abf810edd562fa521955444e2dbef26ff40d' +) + +scripts=( + ['postinstall']='postinstall.sh' + ['postremove']='postremove.sh' +) + +package() { + cd "${srcdir}" + + mkdir -p "${srcdir}/extracted" + ar x yandex-browser.deb + tar -xf data.tar.* -C "${srcdir}/extracted" + + install -dm755 "${pkgdir}/opt/yandex" + cp -a "${srcdir}/extracted/opt/yandex/browser" "${pkgdir}/opt/yandex/" + chmod 0755 "${pkgdir}/opt/yandex/browser/yandex_browser-sandbox" + + install -dm755 "${pkgdir}/usr/bin" + ln -sfn /opt/yandex/browser/yandex-browser \ + "${pkgdir}/usr/bin/yandex-browser-stable" + ln -sfn yandex-browser-stable "${pkgdir}/usr/bin/yandex-browser" + + install -dm755 "${pkgdir}/usr/share" + cp -a "${srcdir}/extracted/usr/share/applications" "${pkgdir}/usr/share/" + compatibility_desktop="${pkgdir}/usr/share/applications/yandex-browser.desktop" + canonical_desktop="${pkgdir}/usr/share/applications/ru.yandex.desktop.browser.desktop" + sed -i '/^NoDisplay=true$/d' "${compatibility_desktop}" "${canonical_desktop}" + sed -i '/^\[Desktop Entry\]$/a NoDisplay=true' "${compatibility_desktop}" + cp -a "${srcdir}/extracted/usr/share/appdata" "${pkgdir}/usr/share/" + cp -a "${srcdir}/extracted/usr/share/gnome-control-center" \ + "${pkgdir}/usr/share/" + cp -a "${srcdir}/extracted/usr/share/man" "${pkgdir}/usr/share/" + cp -a "${srcdir}/extracted/usr/share/mime" "${pkgdir}/usr/share/" + cp -a "${srcdir}/extracted/usr/share/icons" "${pkgdir}/usr/share/" + + install -Dm644 \ + "${srcdir}/extracted/usr/share/doc/yandex-browser-stable/changelog.gz" \ + "${pkgdir}/usr/share/doc/yandex-browser-stable/changelog.gz" + + for size in 16 24 32 48 64 128 256; do + install -Dm644 \ + "${pkgdir}/opt/yandex/browser/product_logo_${size}.png" \ + "${pkgdir}/usr/share/icons/hicolor/${size}x${size}/apps/yandex-browser.png" + done + install -Dm644 "${pkgdir}/opt/yandex/browser/product_logo_128.png" \ + "${pkgdir}/usr/share/pixmaps/yandex-browser.png" + + install-license LICENSE "yandex-browser-stable/LICENSE.nivora" +} + +files() { + files-find "/opt/yandex/browser/**/*" + files-find-binary + files-find-desktop + files-find-license + files-find \ + "/usr/share/appdata/yandex-browser.appdata.xml" \ + "/usr/share/doc/yandex-browser-stable/changelog.gz" \ + "/usr/share/gnome-control-center/default-apps/yandex-browser.xml" \ + "/usr/share/icons/hicolor/*/apps/yandex-browser.png" \ + "/usr/share/icons/hicolor/*/mimetypes/application-x-yprotect.*" \ + "/usr/share/man/man1/yandex-browser*.1.gz" \ + "/usr/share/mime/packages/yandex-browser-yprotect.xml" \ + "/usr/share/pixmaps/yandex-browser.png" +} diff --git a/yandex-browser-stable/postinstall.sh b/yandex-browser-stable/postinstall.sh new file mode 100755 index 0000000..e23044d --- /dev/null +++ b/yandex-browser-stable/postinstall.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +sandbox=/opt/yandex/browser/yandex_browser-sandbox +if [[ ! -f "$sandbox" ]]; then + echo "Ошибка: не найден Chromium sandbox: ${sandbox}" >&2 + exit 1 +fi +if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \ + && unshare --user true 2>/dev/null; then + chmod 0755 "$sandbox" +else + chmod 4755 "$sandbox" +fi + +codec_dir=/opt/yandex/browser +if [[ -x "${codec_dir}/update_codecs" ]]; then + rm -f -- "${codec_dir}/libffmpeg.so" "${codec_dir}/codecs_checksum" + if ! "${codec_dir}/update_codecs" "$codec_dir"; then + echo "Предупреждение: не удалось установить дополнительные медиакодеки." >&2 + fi +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v update-mime-database >/dev/null 2>&1; then + optional_refresh "базу MIME" update-mime-database /usr/share/mime +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/yandex-browser-stable/postremove.sh b/yandex-browser-stable/postremove.sh new file mode 100755 index 0000000..061dadb --- /dev/null +++ b/yandex-browser-stable/postremove.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -euo pipefail + +optional_refresh() { + local label=$1 + shift + if ! "$@"; then + echo "Предупреждение: не удалось обновить ${label}." >&2 + fi +} + +if command -v update-desktop-database >/dev/null 2>&1; then + optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications +fi +if command -v update-mime-database >/dev/null 2>&1; then + optional_refresh "базу MIME" update-mime-database /usr/share/mime +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor +fi +if command -v kbuildsycoca6 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca6 --noincremental +elif command -v kbuildsycoca5 >/dev/null 2>&1; then + optional_refresh "кэш KDE" kbuildsycoca5 --noincremental +fi diff --git a/yandex-browser-stable/stapler-repo.toml b/yandex-browser-stable/stapler-repo.toml new file mode 100644 index 0000000..9c222ce --- /dev/null +++ b/yandex-browser-stable/stapler-repo.toml @@ -0,0 +1 @@ +include = "../stapler-repo.toml"