feat(proxy-client): добавлена самостоятельная установка
- Зачем: - пакет должен устанавливаться безопасно без полного dotfiles и обязательного Stow. - Что: - в master влита feature-ветка с установщиком, тестами и цельной инструкцией. - история проектирования сохранена, а рабочая зона .scratch удалена перед слиянием. - Проверка: - bash tests/test_proxy_client_install.sh. - bash tests/test_install.sh.
This commit is contained in:
commit
ea5c7afd4c
7 files changed
+822
-53
No files matched your search
Whitespace-only changes.
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
Дата: 2026-07-15
|
Дата: 2026-07-15
|
||||||
|
|
||||||
Статус: реализация готова к ручной приёмке; публичное имя уточнено 2026-07-21
|
Статус: основная реализация принята вручную; самостоятельный установщик
|
||||||
|
спроектирован 2026-07-21, но ещё не реализован
|
||||||
|
|
||||||
## Проблема
|
## Проблема
|
||||||
|
|
||||||
@@ -48,8 +49,9 @@ proxy-переменных совместима с локальным Xray бе
|
|||||||
JSON Xray.
|
JSON Xray.
|
||||||
- Сохранить совместимость Bash-функций с внешним Hiddify.
|
- Сохранить совместимость Bash-функций с внешним Hiddify.
|
||||||
- Позволить настраивать машинный `NO_PROXY` без правки файлов репозитория.
|
- Позволить настраивать машинный `NO_PROXY` без правки файлов репозитория.
|
||||||
- Поддержать выборочную установку после `git clone`: через stow, ручные
|
- Поддержать самостоятельную установку из отдельно скопированного каталога
|
||||||
симлинки или копирование.
|
`proxy-client/` без зависимости от общего установщика dotfiles и без
|
||||||
|
обязательного stow.
|
||||||
|
|
||||||
## Не входит в объём
|
## Не входит в объём
|
||||||
|
|
||||||
@@ -89,6 +91,22 @@ Bash нужен только там, где необходимо изменит
|
|||||||
требует кода, но не решает основную боль: безопасный запуск `pclaude` и
|
требует кода, но не решает основную боль: безопасный запуск `pclaude` и
|
||||||
`pcodex` из точечной ссылки Hiddify.
|
`pcodex` из точечной ссылки Hiddify.
|
||||||
|
|
||||||
|
### Доставка отдельного пакета
|
||||||
|
|
||||||
|
Рассмотрены четыре способа подключить отдельно скопированный каталог:
|
||||||
|
|
||||||
|
- оставить только инструкции для stow и ручных симлинков — просто, но сохраняет
|
||||||
|
легко забываемые шаги;
|
||||||
|
- копировать файлы в домашний каталог — не требует сохранять исходный каталог,
|
||||||
|
но усложняет обновления и расходится с моделью dotfiles;
|
||||||
|
- требовать stow — даёт привычное управление ссылками, но добавляет
|
||||||
|
необязательную зависимость на удалённых серверах;
|
||||||
|
- дать пакету собственный установщик, который использует stow при наличии и
|
||||||
|
создаёт те же симлинки напрямую без него.
|
||||||
|
|
||||||
|
Выбран последний вариант. Он сохраняет один результат установки при разном
|
||||||
|
составе машины и не требует второй копии обновляемых файлов.
|
||||||
|
|
||||||
## Рекомендуемое решение
|
## Рекомендуемое решение
|
||||||
|
|
||||||
### Границы компонентов
|
### Границы компонентов
|
||||||
@@ -376,15 +394,50 @@ CLI раскладывается как `~/.local/bin/proxy-client`. Машин
|
|||||||
`~/.config/proxy-client/`. Шаблоны не подменяют настоящие пользовательские
|
`~/.config/proxy-client/`. Шаблоны не подменяют настоящие пользовательские
|
||||||
файлы.
|
файлы.
|
||||||
|
|
||||||
В корне этого каталога находится самостоятельный `README.md`. Он является
|
В корне этого каталога находятся самостоятельный `README.md` и пакетный
|
||||||
основной инструкцией клиента и исключается из раскладки stow. README остаётся
|
`install.sh`. Оба файла относятся к исходному каталогу и исключаются из
|
||||||
рядом с кодом при выборочном клонировании или копировании каталога. Корневой
|
раскладки stow. README остаётся рядом с кодом при выборочном клонировании или
|
||||||
README репозитория только кратко описывает подсистему и ссылается на него.
|
копировании каталога. Корневой README репозитория только кратко описывает
|
||||||
|
подсистему и ссылается на него.
|
||||||
|
|
||||||
После клонирования репозитория пакет можно подключить отдельно от общего
|
Пакетный установщик связывает отдельно скопированный каталог с домашним
|
||||||
`install.sh`. README также должен описывать эквивалентные ручные симлинки и
|
каталогом и не зависит от положения или наличия остальных dotfiles. Если stow
|
||||||
копирование файлов. Полный `install.sh` подключает этот же stow-пакет вместе с
|
доступен, установщик использует его; если нет — создаёт тот же набор симлинков
|
||||||
остальными dotfiles и не имеет отдельной реализации.
|
напрямую. Выбранный механизм явно показывается пользователю, но не меняет
|
||||||
|
результат. Исходный каталог должен оставаться на прежнем пути всё время
|
||||||
|
использования ссылок.
|
||||||
|
|
||||||
|
Установщик управляет только тремя публичными артефактами: CLI, `shell.bash` и
|
||||||
|
каталогом шаблонов. Машинный `config` и закрытый `default.url` не входят в
|
||||||
|
раскладку stow, никогда не создаются, не заменяются и не связываются
|
||||||
|
установщиком. Их наличие в исходном каталоге также не должно расширять набор
|
||||||
|
управляемых путей.
|
||||||
|
|
||||||
|
Домашние `~/.local/bin` и `~/.config/proxy-client` остаются настоящими
|
||||||
|
каталогами. Stow не должен сворачивать их или их родителей в одну ссылку на
|
||||||
|
исходное дерево: ссылками становятся только три перечисленных артефакта. Это
|
||||||
|
одновременно сохраняет место для машинных файлов и делает результат stow
|
||||||
|
совпадающим с прямой раскладкой через `ln -s`.
|
||||||
|
|
||||||
|
Перед записью установщик проверяет весь набор назначений. Правильная ссылка на
|
||||||
|
текущий исходный каталог считается уже установленной; любой другой файл,
|
||||||
|
каталог или симлинк считается конфликтом. При конфликте установщик ничего не
|
||||||
|
меняет и сообщает все пути, которые пользователь должен разобрать вручную.
|
||||||
|
Принудительной замены и автоматических резервных копий нет.
|
||||||
|
|
||||||
|
По умолчанию установщик идемпотентно подключает Bash-модуль в `~/.bashrc` и при
|
||||||
|
необходимости добавляет `~/.local/bin` в `PATH`. Уже существующее подключение
|
||||||
|
через dotfiles не дублируется. Управляемый блок помечается, повторно не
|
||||||
|
добавляется и может быть полностью отключён параметром `--no-shell-setup`.
|
||||||
|
|
||||||
|
Установщик не выбирает режим прокси, не создаёт конфигурацию, не читает VLESS,
|
||||||
|
не устанавливает Xray и не проверяет сеть. После раскладки он проверяет только
|
||||||
|
локальные требования и исправность CLI и Bash-модуля, затем печатает краткую
|
||||||
|
подсказку для текущей shell-сессии и направляет пользователя в README за
|
||||||
|
настройкой режима. Недоступный прокси не делает установку неуспешной.
|
||||||
|
|
||||||
|
Полный корневой `install.sh` продолжает подключать тот же stow-пакет вместе с
|
||||||
|
остальными dotfiles и не получает отдельной реализации этой логики.
|
||||||
|
|
||||||
## Решения и отклонённые альтернативы
|
## Решения и отклонённые альтернативы
|
||||||
|
|
||||||
@@ -412,6 +465,14 @@ README репозитория только кратко описывает по
|
|||||||
автоматическое распределение портов отложены до появления реальной нужды.
|
автоматическое распределение портов отложены до появления реальной нужды.
|
||||||
- Официальный системный установщик отклонён: он требует root и создаёт
|
- Официальный системный установщик отклонён: он требует root и создаёт
|
||||||
systemd-службы. Пользовательская установка соответствует выбранной модели.
|
systemd-службы. Пользовательская установка соответствует выбранной модели.
|
||||||
|
- Копирование файлов пакета отклонено: оно создаёт отдельные экземпляры,
|
||||||
|
усложняет обновление и нарушает общую с dotfiles модель симлинков.
|
||||||
|
- Обязательный stow отклонён как лишняя зависимость. Прямые симлинки дают тот же
|
||||||
|
результат на машине без stow, а автоматический выбор механизма не меняет
|
||||||
|
публичное поведение пакета.
|
||||||
|
- Автоматическая настройка режима, установка Xray и проверка сети отклонены для
|
||||||
|
установщика пакета. Эти операции зависят от машины, могут требовать секретов
|
||||||
|
или сети и уже имеют отдельные явные команды и инструкции.
|
||||||
- Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная
|
- Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная
|
||||||
VLESS-ссылка проще для пользователя и ограничивает область преобразования.
|
VLESS-ссылка проще для пользователя и ограничивает область преобразования.
|
||||||
|
|
||||||
@@ -428,6 +489,15 @@ README репозитория только кратко описывает по
|
|||||||
истинное значение сопровождается предупреждением.
|
истинное значение сопровождается предупреждением.
|
||||||
- Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до
|
- Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до
|
||||||
основной работы и сообщает обнаруженную и требуемую версии.
|
основной работы и сообщает обнаруженную и требуемую версии.
|
||||||
|
- Симлинки зависят от постоянного пути исходного каталога. Установщик явно
|
||||||
|
сообщает эту зависимость и при повторном запуске принимает только ссылки на
|
||||||
|
текущий источник.
|
||||||
|
- Изменение `~/.bashrc` может конфликтовать с пользовательским способом загрузки
|
||||||
|
оболочки. Установщик распознаёт собственный блок и известное подключение из
|
||||||
|
dotfiles, не дублирует их и предоставляет `--no-shell-setup`.
|
||||||
|
- Частичная установка может оставить неоднозначное состояние. Все конфликты
|
||||||
|
назначений проверяются до изменений; проверки охватывают отсутствие новых
|
||||||
|
ссылок и правок shell при отказе.
|
||||||
- `NO_PROXY` реализуется программами неодинаково. README рекомендует точные IP
|
- `NO_PROXY` реализуется программами неодинаково. README рекомендует точные IP
|
||||||
и домены, а CIDR описывает как совместимость best-effort.
|
и домены, а CIDR описывает как совместимость best-effort.
|
||||||
- Разные пользователи делят сетевое пространство имён. Настраиваемый порт и
|
- Разные пользователи делят сетевое пространство имён. Настраиваемый порт и
|
||||||
@@ -463,6 +533,22 @@ README репозитория только кратко описывает по
|
|||||||
- Регрессионные Bash-тесты подтверждают прежнее поведение Hiddify при отсутствии
|
- Регрессионные Bash-тесты подтверждают прежнее поведение Hiddify при отсутствии
|
||||||
машинного конфига, оба регистра proxy-переменных, настраиваемый `NO_PROXY` и
|
машинного конфига, оба регистра proxy-переменных, настраиваемый `NO_PROXY` и
|
||||||
отсутствие автоматического запроса внешнего IP в `proxy_on`/`proxy_off`.
|
отсутствие автоматического запроса внешнего IP в `proxy_on`/`proxy_off`.
|
||||||
|
- Проверки самостоятельной установки покрывают путь со stow и без него,
|
||||||
|
одинаковый набор итоговых ссылок, запуск не из каталога пакета и каталог с
|
||||||
|
пробелами в пути.
|
||||||
|
- В обоих путях домашние каталоги остаются обычными каталогами, а пакетные
|
||||||
|
`README.md` и `install.sh` не появляются в домашнем каталоге через stow.
|
||||||
|
- Повторный запуск не меняет уже правильную установку. Конфликт любого
|
||||||
|
назначения обнаруживается до записи; чужие файлы, каталоги и ссылки остаются
|
||||||
|
неизменными.
|
||||||
|
- Машинный `config` и закрытый `default.url` не управляются установщиком даже
|
||||||
|
при их наличии в исходном каталоге или домашнем каталоге.
|
||||||
|
- Настройка Bash проверяется для нового `~/.bashrc`, уже помеченного блока,
|
||||||
|
подключения из полного dotfiles и режима `--no-shell-setup`. Добавление
|
||||||
|
`~/.local/bin` не дублируется.
|
||||||
|
- Локальная проверка установки не обращается к сети и не требует доступного
|
||||||
|
прокси. Неисправный или отсутствующий машинный конфиг не отменяет успешную
|
||||||
|
раскладку пакета.
|
||||||
- Ручная проверка выполняется минимум на Ubuntu с Python 3.10 и на одной целевой
|
- Ручная проверка выполняется минимум на Ubuntu с Python 3.10 и на одной целевой
|
||||||
удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере
|
удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере
|
||||||
доступности: недоступная среда явно записывается как неподтверждённая, а
|
доступности: недоступная среда явно записывается как неподтверждённая, а
|
||||||
@@ -474,7 +560,11 @@ README репозитория только кратко описывает по
|
|||||||
реализацией. Она должна описывать:
|
реализацией. Она должна описывать:
|
||||||
|
|
||||||
- различие внешнего Hiddify и управляемого Xray;
|
- различие внешнего Hiddify и управляемого Xray;
|
||||||
- выборочная установка через stow, ручные симлинки и копирование;
|
- самостоятельный `install.sh`, его автоматический выбор stow или прямых
|
||||||
|
симлинков, требование сохранять исходный каталог и режим
|
||||||
|
`--no-shell-setup`;
|
||||||
|
- границу между управляемыми файлами пакета и создаваемыми вручную машинным
|
||||||
|
конфигом и закрытым профилем;
|
||||||
- создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
|
- создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
|
||||||
- команды управления, `proxy_on/off`, `pclaude/pcodex`;
|
- команды управления, `proxy_on/off`, `pclaude/pcodex`;
|
||||||
- настройка `NO_PROXY`, ограничения CIDR и диагностика;
|
- настройка `NO_PROXY`, ограничения CIDR и диагностика;
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
^/README\.md$
|
^/README\.md$
|
||||||
|
^/install\.sh$
|
||||||
|
^/\.config/proxy-client/config$
|
||||||
|
^/\.config/proxy-client/default\.url$
|
||||||
+98
-41
@@ -10,41 +10,68 @@ CLI требует Python 3.8+ и использует только станда
|
|||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
|
### Только пакет `proxy-client`
|
||||||
|
|
||||||
|
Скопируйте каталог в постоянное место и запустите пакетный установщик. Текущий
|
||||||
|
рабочий каталог не важен:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/src/proxy-client/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
При наличии Stow установщик использует его, иначе создаёт те же три симлинка
|
||||||
|
напрямую: CLI, Bash-модуль и каталог шаблонов. Исходный каталог после установки
|
||||||
|
нельзя перемещать: домашние ссылки зависят от его постоянного пути.
|
||||||
|
|
||||||
|
По умолчанию установщик добавляет в `~/.bashrc` помеченный блок для
|
||||||
|
`~/.local/bin` и Bash-модуля. После установки откройте новый Bash или примените
|
||||||
|
блок в текущей сессии:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы оставить `~/.bashrc` без изменений, запустите:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/src/proxy-client/install.sh --no-shell-setup
|
||||||
|
```
|
||||||
|
|
||||||
|
В этом случае подготовьте только текущую сессию вручную:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
source ~/.config/proxy-client/shell.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Для следующих сессий добавьте эти строки в подходящий Bash-конфиг сами.
|
||||||
|
|
||||||
|
Установщик не создаёт машинный `config` и закрытый `default.url`, не
|
||||||
|
устанавливает Xray и не проверяет сеть. После подключения команд выберите режим
|
||||||
|
по разделу [«Настройка режима»](#настройка-режима). Без машинного конфига
|
||||||
|
действует режим Hiddify по умолчанию.
|
||||||
|
|
||||||
### В составе dotfiles
|
### В составе dotfiles
|
||||||
|
|
||||||
Полная установка подключает пакет и автоматически загружает Bash-функции в
|
Корневой установщик dotfiles подключает тот же пакет через Stow и загружает
|
||||||
новых интерактивных сессиях:
|
Bash-функции в новых интерактивных сессиях:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/dotfiles
|
cd ~/dotfiles
|
||||||
./install.sh
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Установщик заранее создаёт `~/.local/bin` и `~/.config/proxy-client`, чтобы Stow
|
Он заранее создаёт `~/.local/bin` и `~/.config/proxy-client`, чтобы Stow
|
||||||
размещал ссылки внутри них, а не превращал сами каталоги в ссылки на
|
размещал ссылки внутри них, а не превращал сами каталоги в ссылки на
|
||||||
репозиторий.
|
репозиторий. После установки откройте новый Bash или выполните:
|
||||||
|
|
||||||
После установки откройте новый Bash или выполните:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
source ~/.bash_custom
|
source ~/.bash_custom
|
||||||
```
|
```
|
||||||
|
|
||||||
### Только пакет `proxy-client`
|
Если `~/.local/bin` отсутствует в `PATH` и в новых сессиях, добавьте ту же строку
|
||||||
|
`export` в свой Bash-конфиг: корневой установщик dotfiles его не изменяет.
|
||||||
```bash
|
|
||||||
cd ~/dotfiles
|
|
||||||
mkdir -p ~/.local/bin ~/.config/proxy-client
|
|
||||||
stow proxy-client
|
|
||||||
source ~/.config/proxy-client/shell.bash
|
|
||||||
```
|
|
||||||
|
|
||||||
Если Bash-пакет dotfiles не установлен, добавьте подключение модуля в
|
|
||||||
`~/.bashrc`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
printf '%s\n' 'source ~/.config/proxy-client/shell.bash' >> ~/.bashrc
|
|
||||||
```
|
|
||||||
|
|
||||||
## Как выбирается прокси
|
## Как выбирается прокси
|
||||||
|
|
||||||
@@ -80,28 +107,46 @@ proxy-client status
|
|||||||
pclaude --version
|
pclaude --version
|
||||||
```
|
```
|
||||||
|
|
||||||
В WSL NAT mode IP Windows-шлюза меняется при перезапуске. Интерактивный
|
#### WSL NAT mode
|
||||||
`./install.sh` предлагает добавить обновление `hproxy.local` в `/etc/wsl.conf`.
|
|
||||||
То же можно настроить вручную:
|
|
||||||
|
|
||||||
```ini
|
IP Windows-шлюза меняется при перезапуске WSL, поэтому `hproxy.local` нужно
|
||||||
[boot]
|
обновлять. В отдельно скопированном пакете системных помощников нет. Узнайте
|
||||||
command = "/path/to/dotfiles/scripts/setup-hproxy.sh"
|
адрес шлюза:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ip route show default | awk 'NR == 1 {print $3}'
|
||||||
```
|
```
|
||||||
|
|
||||||
После изменения выполните `wsl --shutdown` из Windows. Обновить адрес без
|
Затем откройте `/etc/hosts` через `sudoedit` и оставьте одну отдельную строку с
|
||||||
перезапуска WSL можно так:
|
полученным адресом:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudoedit /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
<адрес-шлюза> hproxy.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Не удаляйте другие имена на той же строке: при необходимости перенесите их на
|
||||||
|
отдельную строку. Повторяйте обновление после перезапуска WSL.
|
||||||
|
|
||||||
|
С полным dotfiles запустите корневой `~/dotfiles/install.sh` интерактивно: он
|
||||||
|
предложит автоматическое обновление при старте WSL. Обновить адрес сразу можно
|
||||||
|
так:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ~/dotfiles/scripts/setup-hproxy.sh
|
sudo ~/dotfiles/scripts/setup-hproxy.sh
|
||||||
# или после загрузки Bash-функций:
|
# после настройки автозапуска и загрузки общих Bash-функций:
|
||||||
update_hproxy
|
update_hproxy
|
||||||
```
|
```
|
||||||
|
|
||||||
В NAT mode Hiddify должен слушать адрес, доступный через Windows-шлюз, а не
|
В NAT mode Hiddify должен слушать адрес, доступный через Windows-шлюз, а не
|
||||||
только `127.0.0.1`.
|
только `127.0.0.1`.
|
||||||
|
|
||||||
В [WSL mirrored mode](https://learn.microsoft.com/windows/wsl/networking#mirrored-mode-networking)
|
#### WSL mirrored mode
|
||||||
|
|
||||||
|
В [mirrored mode](https://learn.microsoft.com/windows/wsl/networking#mirrored-mode-networking)
|
||||||
(`networkingMode=mirrored`) Windows-сервисы доступны из WSL через `127.0.0.1`.
|
(`networkingMode=mirrored`) Windows-сервисы доступны из WSL через `127.0.0.1`.
|
||||||
Чтобы сохранить адрес по умолчанию, добавьте алиас в Windows hosts:
|
Чтобы сохранить адрес по умолчанию, добавьте алиас в Windows hosts:
|
||||||
|
|
||||||
@@ -110,9 +155,9 @@ update_hproxy
|
|||||||
127.0.0.1 hproxy.local
|
127.0.0.1 hproxy.local
|
||||||
```
|
```
|
||||||
|
|
||||||
Альтернатива — создать `config.external` с `address = 127.0.0.1`. В обоих
|
Альтернатива — настройте внешний режим по следующему разделу и укажите
|
||||||
случаях проверьте результат через `getent hosts hproxy.local` или
|
`address = 127.0.0.1`. Алиас проверяйте через `getent hosts hproxy.local`, а
|
||||||
`proxy-client status`.
|
активный конфиг — через `proxy-client status`.
|
||||||
|
|
||||||
### Другой внешний прокси
|
### Другой внешний прокси
|
||||||
|
|
||||||
@@ -158,7 +203,7 @@ chmod 600 ~/.config/proxy-client/default.url
|
|||||||
proxy-client install
|
proxy-client install
|
||||||
```
|
```
|
||||||
|
|
||||||
Запустите Xray в отдельной панели tmux:
|
Запустите Xray в отдельном терминале или панели tmux:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
proxy-client start
|
proxy-client start
|
||||||
@@ -247,20 +292,32 @@ export PROXY_PREFLIGHT_TIMEOUT=5
|
|||||||
|
|
||||||
## Справочник
|
## Справочник
|
||||||
|
|
||||||
### Ручное подключение без Stow
|
### Ручное подключение без установщика
|
||||||
|
|
||||||
Можно создать ссылки на CLI, Bash-модуль и примеры либо скопировать те же файлы:
|
Если пакетный установщик запустить нельзя, выберите постоянный абсолютный путь
|
||||||
|
к исходному каталогу и создайте те же ссылки вручную. Команды предполагают, что
|
||||||
|
назначения ещё не заняты:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.local/bin ~/.config/proxy-client
|
mkdir -p ~/.local/bin ~/.config/proxy-client
|
||||||
ln -s /path/to/proxy-client/.local/bin/proxy-client ~/.local/bin/proxy-client
|
ln -s /absolute/path/to/proxy-client/.local/bin/proxy-client \
|
||||||
ln -s /path/to/proxy-client/.config/proxy-client/shell.bash \
|
~/.local/bin/proxy-client
|
||||||
|
ln -s /absolute/path/to/proxy-client/.config/proxy-client/shell.bash \
|
||||||
~/.config/proxy-client/shell.bash
|
~/.config/proxy-client/shell.bash
|
||||||
ln -s /path/to/proxy-client/.config/proxy-client/examples \
|
ln -s /absolute/path/to/proxy-client/.config/proxy-client/examples \
|
||||||
~/.config/proxy-client/examples
|
~/.config/proxy-client/examples
|
||||||
|
chmod 755 /absolute/path/to/proxy-client/.local/bin/proxy-client
|
||||||
```
|
```
|
||||||
|
|
||||||
Исполняемому файлу нужен режим `755`, а `~/.local/bin` должен входить в `PATH`.
|
Подготовьте текущую Bash-сессию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
source ~/.config/proxy-client/shell.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Для следующих сессий добавьте эти две строки в свой Bash-конфиг. Исходный
|
||||||
|
каталог после этого нельзя перемещать: созданные ссылки зависят от его пути.
|
||||||
|
|
||||||
### Машинный конфиг
|
### Машинный конфиг
|
||||||
|
|
||||||
|
|||||||
Executable
+216
@@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
STOW_BIN="${STOW_BIN:-stow}"
|
||||||
|
SETUP_SHELL=1
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Использование: ./install.sh [--no-shell-setup]
|
||||||
|
|
||||||
|
--no-shell-setup не изменять ~/.bashrc
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--no-shell-setup)
|
||||||
|
SETUP_SHELL=0
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Ошибка: неизвестный параметр: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
SOURCES=(
|
||||||
|
"$PACKAGE_DIR/.local/bin/proxy-client"
|
||||||
|
"$PACKAGE_DIR/.config/proxy-client/shell.bash"
|
||||||
|
"$PACKAGE_DIR/.config/proxy-client/examples"
|
||||||
|
)
|
||||||
|
TARGETS=(
|
||||||
|
"$HOME/.local/bin/proxy-client"
|
||||||
|
"$HOME/.config/proxy-client/shell.bash"
|
||||||
|
"$HOME/.config/proxy-client/examples"
|
||||||
|
)
|
||||||
|
STOW_IGNORE_PATTERNS=(
|
||||||
|
'^\.local/bin/proxy-client$'
|
||||||
|
'^\.config/proxy-client/shell\.bash$'
|
||||||
|
'^\.config/proxy-client/examples$'
|
||||||
|
)
|
||||||
|
|
||||||
|
source_errors=()
|
||||||
|
[ -x "${SOURCES[0]}" ] || source_errors+=("${SOURCES[0]}")
|
||||||
|
[ -f "${SOURCES[1]}" ] || source_errors+=("${SOURCES[1]}")
|
||||||
|
[ -d "${SOURCES[2]}" ] || source_errors+=("${SOURCES[2]}")
|
||||||
|
if [ "${#source_errors[@]}" -gt 0 ]; then
|
||||||
|
echo "Ошибка: отсутствуют обязательные файлы исходного пакета:" >&2
|
||||||
|
printf ' %s\n' "${source_errors[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${SOURCES[0]}" --help >/dev/null
|
||||||
|
bash -n "${SOURCES[1]}"
|
||||||
|
|
||||||
|
SHELL_BLOCK_START='# >>> proxy-client >>>'
|
||||||
|
SHELL_BLOCK_END='# <<< proxy-client <<<'
|
||||||
|
SHELL_SETUP_NEEDED=0
|
||||||
|
BASH_CUSTOM_HAS_MODULE=0
|
||||||
|
SHELL_PATH_NEEDED=1
|
||||||
|
SHELL_SOURCE_NEEDED=1
|
||||||
|
SHELL_BLOCK_CONFLICT=0
|
||||||
|
if [ "$SETUP_SHELL" -eq 1 ]; then
|
||||||
|
if [ -f "$HOME/.bash_custom" ] \
|
||||||
|
&& grep -Fq 'proxy-client/shell.bash' "$HOME/.bash_custom"; then
|
||||||
|
BASH_CUSTOM_HAS_MODULE=1
|
||||||
|
SHELL_SOURCE_NEEDED=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
bashrc_start_count=0
|
||||||
|
bashrc_end_count=0
|
||||||
|
if [ -f "$HOME/.bashrc" ]; then
|
||||||
|
bashrc_start_count="$(
|
||||||
|
grep -Fxc "$SHELL_BLOCK_START" "$HOME/.bashrc" || true
|
||||||
|
)"
|
||||||
|
bashrc_end_count="$(
|
||||||
|
grep -Fxc "$SHELL_BLOCK_END" "$HOME/.bashrc" || true
|
||||||
|
)"
|
||||||
|
if grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then
|
||||||
|
SHELL_PATH_NEEDED=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$bashrc_start_count" -eq 0 ] && [ "$bashrc_end_count" -eq 0 ]; then
|
||||||
|
if [ "$SHELL_PATH_NEEDED" -eq 1 ] \
|
||||||
|
|| [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then
|
||||||
|
SHELL_SETUP_NEEDED=1
|
||||||
|
fi
|
||||||
|
elif [ "$bashrc_start_count" -eq 1 ] \
|
||||||
|
&& [ "$bashrc_end_count" -eq 1 ] \
|
||||||
|
&& [ "$SHELL_PATH_NEEDED" -eq 0 ] \
|
||||||
|
&& { [ "$BASH_CUSTOM_HAS_MODULE" -eq 1 ] \
|
||||||
|
|| grep -Fq 'proxy-client/shell.bash' "$HOME/.bashrc"; }; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
SHELL_BLOCK_CONFLICT=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
conflicts=()
|
||||||
|
missing_targets=0
|
||||||
|
stow_ignore_args=()
|
||||||
|
PARENT_DIRS=(
|
||||||
|
"$HOME/.local"
|
||||||
|
"$HOME/.local/bin"
|
||||||
|
"$HOME/.config"
|
||||||
|
"$HOME/.config/proxy-client"
|
||||||
|
)
|
||||||
|
for parent_dir in "${PARENT_DIRS[@]}"; do
|
||||||
|
if { [ -e "$parent_dir" ] || [ -L "$parent_dir" ]; } \
|
||||||
|
&& { [ ! -d "$parent_dir" ] || [ -L "$parent_dir" ]; }; then
|
||||||
|
conflicts+=("$parent_dir")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for index in "${!TARGETS[@]}"; do
|
||||||
|
target="${TARGETS[$index]}"
|
||||||
|
source_path="${SOURCES[$index]}"
|
||||||
|
if [ -e "$target" ] || [ -L "$target" ]; then
|
||||||
|
if [ ! -L "$target" ] \
|
||||||
|
|| [ "$(readlink -f "$target" 2>/dev/null || true)" != "$source_path" ]; then
|
||||||
|
conflicts+=("$target")
|
||||||
|
else
|
||||||
|
stow_ignore_args+=("--ignore=${STOW_IGNORE_PATTERNS[$index]}")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
missing_targets=$((missing_targets + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$SHELL_SETUP_NEEDED" -eq 1 ] \
|
||||||
|
&& { [ -e "$HOME/.bashrc" ] || [ -L "$HOME/.bashrc" ]; } \
|
||||||
|
&& [ ! -f "$HOME/.bashrc" ]; then
|
||||||
|
conflicts+=("$HOME/.bashrc")
|
||||||
|
fi
|
||||||
|
if [ "$SHELL_BLOCK_CONFLICT" -eq 1 ]; then
|
||||||
|
conflicts+=("$HOME/.bashrc")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#conflicts[@]}" -gt 0 ]; then
|
||||||
|
echo "Ошибка: найдены конфликтующие пути:" >&2
|
||||||
|
printf ' %s\n' "${conflicts[@]}" >&2
|
||||||
|
echo "Разберите их вручную и повторите установку." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.local/bin" "$HOME/.config/proxy-client"
|
||||||
|
|
||||||
|
if command -v "$STOW_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "Установка с помощью Stow."
|
||||||
|
if [ "$missing_targets" -gt 0 ]; then
|
||||||
|
"$STOW_BIN" --dir="$(dirname "$PACKAGE_DIR")" --target="$HOME" \
|
||||||
|
"${stow_ignore_args[@]}" \
|
||||||
|
"$(basename "$PACKAGE_DIR")"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Установка без Stow: создаю символические ссылки напрямую."
|
||||||
|
for index in "${!TARGETS[@]}"; do
|
||||||
|
target="${TARGETS[$index]}"
|
||||||
|
source_path="${SOURCES[$index]}"
|
||||||
|
if [ ! -L "$target" ]; then
|
||||||
|
ln -s "$source_path" "$target"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$HOME/.local/bin/proxy-client" --help >/dev/null
|
||||||
|
bash -n "$HOME/.config/proxy-client/shell.bash"
|
||||||
|
|
||||||
|
if [ "$SHELL_SETUP_NEEDED" -eq 1 ]; then
|
||||||
|
cat >> "$HOME/.bashrc" <<'EOF'
|
||||||
|
|
||||||
|
# >>> proxy-client >>>
|
||||||
|
EOF
|
||||||
|
if [ "$SHELL_PATH_NEEDED" -eq 1 ]; then
|
||||||
|
cat >> "$HOME/.bashrc" <<'EOF'
|
||||||
|
case ":$PATH:" in
|
||||||
|
*":$HOME/.local/bin:"*) ;;
|
||||||
|
*)
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
if [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then
|
||||||
|
cat >> "$HOME/.bashrc" <<'EOF'
|
||||||
|
[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
cat >> "$HOME/.bashrc" <<'EOF'
|
||||||
|
# <<< proxy-client <<<
|
||||||
|
EOF
|
||||||
|
elif [ "$SETUP_SHELL" -eq 0 ]; then
|
||||||
|
echo "Настройка Bash пропущена (--no-shell-setup)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SETUP_SHELL" -eq 1 ]; then
|
||||||
|
RELOAD_COMMAND='source ~/.bashrc'
|
||||||
|
else
|
||||||
|
RELOAD_COMMAND='export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash'
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
proxy-client установлен.
|
||||||
|
Исходный каталог должен оставаться по постоянному пути:
|
||||||
|
$PACKAGE_DIR
|
||||||
|
Примените настройки в текущей сессии: $RELOAD_COMMAND
|
||||||
|
Настройка режима описана в $PACKAGE_DIR/README.md.
|
||||||
|
EOF
|
||||||
@@ -13,5 +13,6 @@ export PYTHONDONTWRITEBYTECODE=1
|
|||||||
|
|
||||||
python3 -m unittest discover -s tests
|
python3 -m unittest discover -s tests
|
||||||
bash tests/test_proxy_launchers.sh
|
bash tests/test_proxy_launchers.sh
|
||||||
|
bash tests/test_proxy_client_install.sh
|
||||||
bash tests/test_install.sh
|
bash tests/test_install.sh
|
||||||
uv run --python 3.8 --no-project python -m unittest discover -s tests
|
uv run --python 3.8 --no-project python -m unittest discover -s tests
|
||||||
Executable
+402
@@ -0,0 +1,402 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
TEST_TMP_ROOT="$(mktemp -d /tmp/proxy-client-install-test.XXXXXX)"
|
||||||
|
trap 'rm -rf "$TEST_TMP_ROOT"' EXIT
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_link_points_to() {
|
||||||
|
local link_path="$1" expected_path="$2" actual_path
|
||||||
|
[ -L "$link_path" ] || fail "Expected symlink: $link_path"
|
||||||
|
actual_path="$(readlink -f "$link_path")"
|
||||||
|
[ "$actual_path" = "$expected_path" ] \
|
||||||
|
|| fail "$link_path points to $actual_path, expected $expected_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_exists() {
|
||||||
|
local path="$1"
|
||||||
|
[ ! -e "$path" ] && [ ! -L "$path" ] || fail "Path should not exist: $path"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_package() {
|
||||||
|
local fixture_root="$1" package_dir="$fixture_root/source with spaces/proxy-client"
|
||||||
|
mkdir -p \
|
||||||
|
"$package_dir/.local/bin" \
|
||||||
|
"$package_dir/.config/proxy-client"
|
||||||
|
cp -a \
|
||||||
|
"$REPO_ROOT/proxy-client/.local/bin/proxy-client" \
|
||||||
|
"$package_dir/.local/bin/proxy-client"
|
||||||
|
cp -a \
|
||||||
|
"$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash" \
|
||||||
|
"$REPO_ROOT/proxy-client/.config/proxy-client/examples" \
|
||||||
|
"$package_dir/.config/proxy-client/"
|
||||||
|
cp -a \
|
||||||
|
"$REPO_ROOT/proxy-client/.stow-local-ignore" \
|
||||||
|
"$REPO_ROOT/proxy-client/README.md" \
|
||||||
|
"$REPO_ROOT/proxy-client/install.sh" \
|
||||||
|
"$package_dir/"
|
||||||
|
printf '%s\n' 'MACHINE_CONFIG_SENTINEL' > "$package_dir/.config/proxy-client/config"
|
||||||
|
printf '%s\n' 'PRIVATE_PROFILE_SENTINEL' > "$package_dir/.config/proxy-client/default.url"
|
||||||
|
printf '%s\n' "$package_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_package_install() {
|
||||||
|
local package_dir="$1" home_dir="$2" stow_bin="$3"
|
||||||
|
shift 3
|
||||||
|
(
|
||||||
|
cd "$TEST_TMP_ROOT"
|
||||||
|
HOME="$home_dir" STOW_BIN="$stow_bin" \
|
||||||
|
"$package_dir/install.sh" "$@"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test_installs_same_public_artifacts_without_stow() {
|
||||||
|
local fixture_root package_dir home_dir output
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
|
||||||
|
output="$(
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow --no-shell-setup
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s' "$output" | grep -Fq 'без Stow' \
|
||||||
|
|| fail "Expected direct-symlink method in output"
|
||||||
|
printf '%s' "$output" | grep -Fq \
|
||||||
|
'export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash' \
|
||||||
|
|| fail "Expected complete current-session hint with --no-shell-setup"
|
||||||
|
[ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \
|
||||||
|
|| fail "Expected ~/.local/bin to be a real directory"
|
||||||
|
[ -d "$home_dir/.config/proxy-client" ] \
|
||||||
|
&& [ ! -L "$home_dir/.config/proxy-client" ] \
|
||||||
|
|| fail "Expected proxy-client config home to be a real directory"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.local/bin/proxy-client" \
|
||||||
|
"$package_dir/.local/bin/proxy-client"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.config/proxy-client/shell.bash" \
|
||||||
|
"$package_dir/.config/proxy-client/shell.bash"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.config/proxy-client/examples" \
|
||||||
|
"$package_dir/.config/proxy-client/examples"
|
||||||
|
assert_not_exists "$home_dir/.config/proxy-client/config"
|
||||||
|
assert_not_exists "$home_dir/.config/proxy-client/default.url"
|
||||||
|
assert_not_exists "$home_dir/README.md"
|
||||||
|
assert_not_exists "$home_dir/install.sh"
|
||||||
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_configures_bash_once_by_default() {
|
||||||
|
local fixture_root package_dir home_dir output marker_count path_count source_count
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
||||||
|
output="$(run_package_install "$package_dir" "$home_dir" missing-stow)"
|
||||||
|
|
||||||
|
marker_count="$(grep -Fxc '# >>> proxy-client >>>' "$home_dir/.bashrc")"
|
||||||
|
path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")"
|
||||||
|
source_count="$(
|
||||||
|
grep -Fc \
|
||||||
|
'[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"' \
|
||||||
|
"$home_dir/.bashrc"
|
||||||
|
)"
|
||||||
|
[ "$marker_count" -eq 1 ] || fail "Expected one managed Bash block"
|
||||||
|
[ "$path_count" -eq 1 ] || fail "Expected one ~/.local/bin PATH line"
|
||||||
|
[ "$source_count" -eq 1 ] || fail "Expected one shell module source line"
|
||||||
|
printf '%s' "$output" | grep -Fq 'source ~/.bashrc' \
|
||||||
|
|| fail "Expected current-session reload hint"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_installs_same_public_artifacts_with_stow() {
|
||||||
|
local fixture_root package_dir home_dir output
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
|
||||||
|
output="$(run_package_install "$package_dir" "$home_dir" stow --no-shell-setup)"
|
||||||
|
|
||||||
|
printf '%s' "$output" | grep -Fq 'с помощью Stow' \
|
||||||
|
|| fail "Expected Stow method in output"
|
||||||
|
[ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \
|
||||||
|
|| fail "Expected ~/.local/bin to be a real directory"
|
||||||
|
[ -d "$home_dir/.config/proxy-client" ] \
|
||||||
|
&& [ ! -L "$home_dir/.config/proxy-client" ] \
|
||||||
|
|| fail "Expected proxy-client config home to be a real directory"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.local/bin/proxy-client" \
|
||||||
|
"$package_dir/.local/bin/proxy-client"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.config/proxy-client/shell.bash" \
|
||||||
|
"$package_dir/.config/proxy-client/shell.bash"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.config/proxy-client/examples" \
|
||||||
|
"$package_dir/.config/proxy-client/examples"
|
||||||
|
assert_not_exists "$home_dir/.config/proxy-client/config"
|
||||||
|
assert_not_exists "$home_dir/.config/proxy-client/default.url"
|
||||||
|
assert_not_exists "$home_dir/README.md"
|
||||||
|
assert_not_exists "$home_dir/install.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_linked_parent_directories_before_changes() {
|
||||||
|
local fixture_root package_dir home_dir external_local external_config output
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
external_local="$fixture_root/external-local"
|
||||||
|
external_config="$fixture_root/external-config"
|
||||||
|
mkdir -p "$home_dir" "$external_local" "$external_config"
|
||||||
|
ln -s "$external_local" "$home_dir/.local"
|
||||||
|
ln -s "$external_config" "$home_dir/.config"
|
||||||
|
|
||||||
|
if output="$(
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
||||||
|
)"; then
|
||||||
|
fail "Expected linked parent directories to be rejected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$output" | grep -Fq "$home_dir/.local" \
|
||||||
|
|| fail "Expected ~/.local conflict in output"
|
||||||
|
printf '%s' "$output" | grep -Fq "$home_dir/.config" \
|
||||||
|
|| fail "Expected ~/.config conflict in output"
|
||||||
|
assert_not_exists "$external_local/bin"
|
||||||
|
assert_not_exists "$external_config/proxy-client"
|
||||||
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_reports_all_target_conflicts_without_partial_install() {
|
||||||
|
local fixture_root package_dir home_dir other_target output
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
other_target="$fixture_root/other-target"
|
||||||
|
mkdir -p "$home_dir/.local/bin" "$home_dir/.config/proxy-client/shell.bash"
|
||||||
|
printf '%s\n' 'KEEP_CLI' > "$home_dir/.local/bin/proxy-client"
|
||||||
|
printf '%s\n' 'KEEP_TARGET' > "$other_target"
|
||||||
|
ln -s "$other_target" "$home_dir/.config/proxy-client/examples"
|
||||||
|
|
||||||
|
if output="$(
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
||||||
|
)"; then
|
||||||
|
fail "Expected all target conflicts to be rejected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|
||||||
|
|| fail "Expected CLI conflict in output"
|
||||||
|
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/shell.bash" \
|
||||||
|
|| fail "Expected shell module conflict in output"
|
||||||
|
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/examples" \
|
||||||
|
|| fail "Expected examples conflict in output"
|
||||||
|
grep -Fq 'KEEP_CLI' "$home_dir/.local/bin/proxy-client" \
|
||||||
|
|| fail "Expected conflicting CLI file to stay unchanged"
|
||||||
|
[ -d "$home_dir/.config/proxy-client/shell.bash" ] \
|
||||||
|
|| fail "Expected conflicting shell directory to stay unchanged"
|
||||||
|
assert_link_points_to "$home_dir/.config/proxy-client/examples" "$other_target"
|
||||||
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_broken_link_without_creating_other_targets() {
|
||||||
|
local fixture_root package_dir home_dir output
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir/.local/bin"
|
||||||
|
ln -s "$fixture_root/missing-cli" "$home_dir/.local/bin/proxy-client"
|
||||||
|
|
||||||
|
if output="$(
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
||||||
|
)"; then
|
||||||
|
fail "Expected broken target link to be rejected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|
||||||
|
|| fail "Expected broken CLI link in output"
|
||||||
|
[ -L "$home_dir/.local/bin/proxy-client" ] \
|
||||||
|
|| fail "Expected broken CLI link to stay unchanged"
|
||||||
|
assert_not_exists "$home_dir/.config"
|
||||||
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_preserves_machine_files_with_both_methods() {
|
||||||
|
local stow_bin fixture_root package_dir home_dir
|
||||||
|
for stow_bin in missing-stow stow; do
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir/.config/proxy-client"
|
||||||
|
printf '%s\n' 'HOME_CONFIG_SENTINEL' \
|
||||||
|
> "$home_dir/.config/proxy-client/config"
|
||||||
|
printf '%s\n' 'HOME_PROFILE_SENTINEL' \
|
||||||
|
> "$home_dir/.config/proxy-client/default.url"
|
||||||
|
|
||||||
|
run_package_install "$package_dir" "$home_dir" "$stow_bin" \
|
||||||
|
--no-shell-setup >/dev/null
|
||||||
|
|
||||||
|
[ ! -L "$home_dir/.config/proxy-client/config" ] \
|
||||||
|
|| fail "Machine config must not become a symlink"
|
||||||
|
[ ! -L "$home_dir/.config/proxy-client/default.url" ] \
|
||||||
|
|| fail "Private profile must not become a symlink"
|
||||||
|
grep -Fq 'HOME_CONFIG_SENTINEL' "$home_dir/.config/proxy-client/config" \
|
||||||
|
|| fail "Machine config changed"
|
||||||
|
grep -Fq 'HOME_PROFILE_SENTINEL' "$home_dir/.config/proxy-client/default.url" \
|
||||||
|
|| fail "Private profile changed"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
test_accepts_existing_links_when_method_changes() {
|
||||||
|
local fixture_root package_dir home_dir
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow \
|
||||||
|
--no-shell-setup >/dev/null
|
||||||
|
run_package_install "$package_dir" "$home_dir" stow \
|
||||||
|
--no-shell-setup >/dev/null
|
||||||
|
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.local/bin/proxy-client" \
|
||||||
|
"$package_dir/.local/bin/proxy-client"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.config/proxy-client/shell.bash" \
|
||||||
|
"$package_dir/.config/proxy-client/shell.bash"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.config/proxy-client/examples" \
|
||||||
|
"$package_dir/.config/proxy-client/examples"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_stow_completes_partial_correct_installation() {
|
||||||
|
local fixture_root package_dir home_dir
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow \
|
||||||
|
--no-shell-setup >/dev/null
|
||||||
|
rm "$home_dir/.config/proxy-client/shell.bash"
|
||||||
|
|
||||||
|
run_package_install "$package_dir" "$home_dir" stow \
|
||||||
|
--no-shell-setup >/dev/null
|
||||||
|
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.local/bin/proxy-client" \
|
||||||
|
"$package_dir/.local/bin/proxy-client"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.config/proxy-client/shell.bash" \
|
||||||
|
"$package_dir/.config/proxy-client/shell.bash"
|
||||||
|
assert_link_points_to \
|
||||||
|
"$home_dir/.config/proxy-client/examples" \
|
||||||
|
"$package_dir/.config/proxy-client/examples"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_does_not_duplicate_full_dotfiles_shell_setup() {
|
||||||
|
local fixture_root package_dir home_dir source_count
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
printf '%s\n' \
|
||||||
|
'[ -f ~/.config/proxy-client/shell.bash ] && source ~/.config/proxy-client/shell.bash' \
|
||||||
|
> "$home_dir/.bash_custom"
|
||||||
|
printf '%s\n' '[ -f ~/.bash_custom ] && source ~/.bash_custom' \
|
||||||
|
> "$home_dir/.bashrc"
|
||||||
|
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
||||||
|
|
||||||
|
grep -Fq '# >>> proxy-client >>>' "$home_dir/.bashrc" \
|
||||||
|
|| fail "Expected managed PATH block with full dotfiles"
|
||||||
|
grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc" \
|
||||||
|
|| fail "Expected ~/.local/bin setup with full dotfiles"
|
||||||
|
source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc" || true)"
|
||||||
|
[ "$source_count" -eq 0 ] \
|
||||||
|
|| fail "Standalone block duplicated full dotfiles module source"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_damaged_managed_bash_block_before_changes() {
|
||||||
|
local fixture_root package_dir home_dir output
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
printf '%s\n' '# >>> proxy-client >>>' > "$home_dir/.bashrc"
|
||||||
|
|
||||||
|
if output="$(
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
||||||
|
)"; then
|
||||||
|
fail "Expected damaged managed Bash block to be rejected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$output" | grep -Fq "$home_dir/.bashrc" \
|
||||||
|
|| fail "Expected ~/.bashrc conflict in output"
|
||||||
|
[ "$(wc -l < "$home_dir/.bashrc")" -eq 1 ] \
|
||||||
|
|| fail "Expected damaged Bash block to stay unchanged"
|
||||||
|
assert_not_exists "$home_dir/.local"
|
||||||
|
assert_not_exists "$home_dir/.config"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_does_not_duplicate_existing_local_bin_path() {
|
||||||
|
local fixture_root package_dir home_dir path_count source_count
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"' > "$home_dir/.bashrc"
|
||||||
|
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
||||||
|
|
||||||
|
path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")"
|
||||||
|
source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc")"
|
||||||
|
[ "$path_count" -eq 1 ] || fail "Existing ~/.local/bin PATH line was duplicated"
|
||||||
|
[ "$source_count" -eq 1 ] || fail "Expected shell module source in managed block"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_incomplete_source_before_changes() {
|
||||||
|
local fixture_root package_dir home_dir output
|
||||||
|
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
|
package_dir="$(setup_package "$fixture_root")"
|
||||||
|
home_dir="$fixture_root/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
rm "$package_dir/.config/proxy-client/shell.bash"
|
||||||
|
|
||||||
|
if output="$(
|
||||||
|
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
||||||
|
)"; then
|
||||||
|
fail "Expected incomplete source package to be rejected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$output" | grep -Fq 'исходного пакета' \
|
||||||
|
|| fail "Expected source package error"
|
||||||
|
assert_not_exists "$home_dir/.local"
|
||||||
|
assert_not_exists "$home_dir/.config"
|
||||||
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
test_installs_same_public_artifacts_without_stow
|
||||||
|
test_configures_bash_once_by_default
|
||||||
|
test_installs_same_public_artifacts_with_stow
|
||||||
|
test_rejects_linked_parent_directories_before_changes
|
||||||
|
test_reports_all_target_conflicts_without_partial_install
|
||||||
|
test_rejects_broken_link_without_creating_other_targets
|
||||||
|
test_preserves_machine_files_with_both_methods
|
||||||
|
test_accepts_existing_links_when_method_changes
|
||||||
|
test_stow_completes_partial_correct_installation
|
||||||
|
test_does_not_duplicate_full_dotfiles_shell_setup
|
||||||
|
test_rejects_damaged_managed_bash_block_before_changes
|
||||||
|
test_does_not_duplicate_existing_local_bin_path
|
||||||
|
test_rejects_incomplete_source_before_changes
|
||||||
|
echo "All standalone proxy-client install tests passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in new issue
Block a user