dploy 0.1.2__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dploy-0.2.0/.gitattributes +5 -0
- dploy-0.2.0/.github/workflows/ci.yml +54 -0
- dploy-0.2.0/.github/workflows/release.yaml +50 -0
- dploy-0.2.0/.gitignore +16 -0
- dploy-0.2.0/AGENTS.md +21 -0
- {dploy-0.1.2 → dploy-0.2.0}/CHANGELOG.md +75 -3
- dploy-0.2.0/CLAUDE.md +1 -0
- dploy-0.2.0/CONTEXT.md +105 -0
- dploy-0.2.0/PKG-INFO +121 -0
- dploy-0.2.0/README.md +101 -0
- dploy-0.2.0/docs/adr/0001-windows-permission-validation.md +67 -0
- dploy-0.2.0/docs/agents/domain.md +51 -0
- dploy-0.2.0/docs/agents/issue-tracker.md +45 -0
- dploy-0.2.0/docs/agents/triage-labels.md +13 -0
- dploy-0.2.0/dploy/__init__.py +94 -0
- {dploy-0.1.2 → dploy-0.2.0}/dploy/__main__.py +2 -4
- dploy-0.2.0/dploy/_winaccess.py +203 -0
- dploy-0.2.0/dploy/actions.py +207 -0
- dploy-0.2.0/dploy/cli.py +173 -0
- dploy-0.2.0/dploy/error.py +175 -0
- dploy-0.2.0/dploy/ignore.py +96 -0
- dploy-0.2.0/dploy/linkcmd.py +119 -0
- dploy-0.2.0/dploy/main.py +138 -0
- dploy-0.2.0/dploy/stowcmd.py +711 -0
- dploy-0.2.0/dploy/utils.py +225 -0
- dploy-0.2.0/dploy/version.py +5 -0
- dploy-0.2.0/pyproject.toml +100 -0
- dploy-0.2.0/tasks.py +123 -0
- dploy-0.2.0/tests/__init__.py +0 -0
- dploy-0.2.0/tests/conftest.py +231 -0
- dploy-0.2.0/tests/test_clean.py +140 -0
- dploy-0.2.0/tests/test_cli.py +159 -0
- dploy-0.2.0/tests/test_ignore.py +228 -0
- dploy-0.2.0/tests/test_link.py +129 -0
- dploy-0.2.0/tests/test_stow.py +499 -0
- dploy-0.2.0/tests/test_unstow.py +517 -0
- dploy-0.2.0/tests/test_utils.py +271 -0
- dploy-0.2.0/tests/utils.py +249 -0
- dploy-0.2.0/uv.lock +476 -0
- dploy-0.1.2/.pylintrc +0 -7
- dploy-0.1.2/MANIFEST.in +0 -12
- dploy-0.1.2/PKG-INFO +0 -76
- dploy-0.1.2/README.md +0 -58
- dploy-0.1.2/dploy/__init__.py +0 -35
- dploy-0.1.2/dploy/__init__.py.orig +0 -45
- dploy-0.1.2/dploy/__init__.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/__init__.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/__init__.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/__main__.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/__main__.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/actions.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/actions.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/cli.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/cli.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/error.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/error.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/ignore.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/ignore.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/linkcmd.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/linkcmd.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/main.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/main.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/stowcmd.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/stowcmd.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/utils.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/utils.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/version.cpython-35.pyc +0 -0
- dploy-0.1.2/dploy/__pycache__/version.cpython-36.pyc +0 -0
- dploy-0.1.2/dploy/actions.py +0 -213
- dploy-0.1.2/dploy/cli.py +0 -101
- dploy-0.1.2/dploy/error.py +0 -121
- dploy-0.1.2/dploy/ignore.py +0 -67
- dploy-0.1.2/dploy/linkcmd.py +0 -87
- dploy-0.1.2/dploy/main.py +0 -127
- dploy-0.1.2/dploy/stowcmd.py +0 -376
- dploy-0.1.2/dploy/utils.py +0 -105
- dploy-0.1.2/dploy/version.py +0 -18
- dploy-0.1.2/dploy.egg-info/PKG-INFO +0 -76
- dploy-0.1.2/dploy.egg-info/SOURCES.txt +0 -73
- dploy-0.1.2/dploy.egg-info/dependency_links.txt +0 -1
- dploy-0.1.2/dploy.egg-info/entry_points.txt +0 -3
- dploy-0.1.2/dploy.egg-info/top_level.txt +0 -1
- dploy-0.1.2/requirements.txt +0 -10
- dploy-0.1.2/setup.cfg +0 -8
- dploy-0.1.2/setup.py +0 -41
- dploy-0.1.2/tasks.py +0 -108
- dploy-0.1.2/tests/__pycache__/conftest.cpython-35-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/conftest.cpython-36-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/conftest.cpython-36.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_cli.cpython-35-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_cli.cpython-36-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_ignore.cpython-35-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_ignore.cpython-36-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_link.cpython-35-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_link.cpython-36-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_stow.cpython-35-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_stow.cpython-36-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_unstow.cpython-35-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/test_unstow.cpython-36-PYTEST.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/utils.cpython-35.pyc +0 -0
- dploy-0.1.2/tests/__pycache__/utils.cpython-36.pyc +0 -0
- dploy-0.1.2/tests/conftest.py +0 -173
- dploy-0.1.2/tests/test_cli.py +0 -84
- dploy-0.1.2/tests/test_ignore.py +0 -40
- dploy-0.1.2/tests/test_link.py +0 -67
- dploy-0.1.2/tests/test_stow.py +0 -232
- dploy-0.1.2/tests/test_unstow.py +0 -228
- dploy-0.1.2/tests/utils.py +0 -101
- {dploy-0.1.2 → dploy-0.2.0}/LICENSE.txt +0 -0
- /dploy-0.1.2/conftest.py → /dploy-0.2.0/dploy/py.typed +0 -0
- {dploy-0.1.2 → dploy-0.2.0}/invoke.yaml +0 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# This checkout is shared between Windows and Linux (via Dropbox), so the same
|
|
2
|
+
# physical files are opened by both. Normalize to LF everywhere -- including in
|
|
3
|
+
# the working tree on Windows -- so that editing from one side does not rewrite
|
|
4
|
+
# every file from the other side's point of view.
|
|
5
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ master ]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
18
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
# hatch-vcs reads the version from tags, so it needs the full history.
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
|
|
26
|
+
- name: Enable symlinks on Windows
|
|
27
|
+
if: runner.os == 'Windows'
|
|
28
|
+
run: |
|
|
29
|
+
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /v "AllowDevelopmentWithoutDevLicense" /d 1 /f
|
|
30
|
+
git config --global core.symlinks true
|
|
31
|
+
|
|
32
|
+
- name: Install uv
|
|
33
|
+
uses: astral-sh/setup-uv@v5
|
|
34
|
+
with:
|
|
35
|
+
enable-cache: true
|
|
36
|
+
cache-dependency-glob: "uv.lock"
|
|
37
|
+
|
|
38
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
39
|
+
run: uv python install ${{ matrix.python-version }}
|
|
40
|
+
|
|
41
|
+
- name: Install dependencies
|
|
42
|
+
run: uv sync --all-groups
|
|
43
|
+
|
|
44
|
+
- name: Lint (pylint, ruff, ty)
|
|
45
|
+
run: uv run invoke lint
|
|
46
|
+
|
|
47
|
+
- name: Reformat Check (ruff)
|
|
48
|
+
run: uv run invoke reformat-check
|
|
49
|
+
|
|
50
|
+
- name: Metrics (radon)
|
|
51
|
+
run: uv run invoke metrics
|
|
52
|
+
|
|
53
|
+
- name: Test (pytest)
|
|
54
|
+
run: uv run invoke test
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag is pushed, for example:
|
|
4
|
+
# git tag 0.2.0 && git push origin 0.2.0
|
|
5
|
+
# The package version comes from the tag (hatch-vcs), so there is no version
|
|
6
|
+
# to edit by hand. Before tagging, set the date in CHANGELOG.md.
|
|
7
|
+
#
|
|
8
|
+
# One-time setup on PyPI: add this repository and workflow (release.yaml) as a
|
|
9
|
+
# trusted publisher for the project, with the environment name "pypi".
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
tags:
|
|
14
|
+
- "[0-9]+.[0-9]+.[0-9]+*"
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
name: Build distributions
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
# hatch-vcs reads the version from tags, so it needs the full history.
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
|
|
26
|
+
- name: Install uv
|
|
27
|
+
uses: astral-sh/setup-uv@v5
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: uv build
|
|
31
|
+
|
|
32
|
+
- uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
name: Publish to PyPI
|
|
39
|
+
needs: build
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
environment: pypi
|
|
42
|
+
permissions:
|
|
43
|
+
id-token: write # required for PyPI trusted publishing
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
dploy-0.2.0/.gitignore
ADDED
dploy-0.2.0/AGENTS.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# dploy
|
|
2
|
+
|
|
3
|
+
A command-line stow/link/unstow utility for managing dotfiles and other
|
|
4
|
+
symlinked file trees.
|
|
5
|
+
|
|
6
|
+
## Agent skills
|
|
7
|
+
|
|
8
|
+
### Issue tracker
|
|
9
|
+
|
|
10
|
+
Issues live as GitHub issues in `arecarn/dploy`, managed with the `gh` CLI. See
|
|
11
|
+
`docs/agents/issue-tracker.md`.
|
|
12
|
+
|
|
13
|
+
### Triage labels
|
|
14
|
+
|
|
15
|
+
Default canonical role names (`needs-triage`, `needs-info`, `ready-for-agent`,
|
|
16
|
+
`ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
|
|
17
|
+
|
|
18
|
+
### Domain docs
|
|
19
|
+
|
|
20
|
+
Single-context: `CONTEXT.md` and `docs/adr/` at the repo root (created lazily,
|
|
21
|
+
absence is not an error). See `docs/agents/domain.md`.
|
|
@@ -6,16 +6,90 @@ All changes documented here should be written with the goal give the end-user
|
|
|
6
6
|
(not other developers) a summarized view of the changes since the last release,
|
|
7
7
|
organized by order of importance.
|
|
8
8
|
|
|
9
|
-
## [
|
|
9
|
+
## [0.2.0] - 2026-11-14
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
13
|
+
- Added a `--skip-conflicts` flag to `stow` that links everything that
|
|
14
|
+
doesn't conflict with an existing, unmanaged file instead of aborting the
|
|
15
|
+
whole command. Dploy exits with status `2` when some files were skipped.
|
|
16
|
+
- Added a `--no-folding` flag to `stow` and `unstow`, matching GNU Stow's
|
|
17
|
+
flag of the same name, that keeps each file as its own individual link
|
|
18
|
+
instead of folding a destination directory into one link.
|
|
19
|
+
- Added a `--dotfiles` flag to `stow` and `unstow`, matching GNU Stow's flag
|
|
20
|
+
of the same name: a package entry named `dot-foo` is linked as `.foo`.
|
|
21
|
+
`unstow` removes links by what they point at, so it does not need the flag.
|
|
22
|
+
|
|
13
23
|
### Changed
|
|
14
24
|
|
|
25
|
+
- **Breaking:** Renamed `source`/`sources`/`dest` to `package`/`packages`/
|
|
26
|
+
`destination` across the public API and the CLI, to match the terms
|
|
27
|
+
`CONTEXT.md` defines. This affects the `stow`, `unstow`, and `clean`
|
|
28
|
+
function parameters and CLI arguments. The `link` command's argument stays
|
|
29
|
+
`source`, since it takes a single file or directory, not a package.
|
|
30
|
+
- Switch to uv for dependency management instead of Poetry.
|
|
31
|
+
- Use hatchling as the build system.
|
|
32
|
+
- Switch CI from Travis CI and AppVeyor to GitHub Actions.
|
|
33
|
+
- Move Pylint configuration to pyproject.toml and remove in-code suppressions.
|
|
34
|
+
|
|
15
35
|
### Fixed
|
|
16
36
|
|
|
37
|
+
- On Windows, a link to a directory is now created as a directory symlink
|
|
38
|
+
instead of a file symlink, which did not work.
|
|
39
|
+
- On Windows, existing links are now recognized as pointing at their
|
|
40
|
+
package. Windows reports link targets with a `\\?\` prefix, which made
|
|
41
|
+
every comparison with the package path fail.
|
|
42
|
+
- On Windows, dploy now decides whether two paths are the same file by asking
|
|
43
|
+
the file system instead of comparing path text, so a different spelling of
|
|
44
|
+
one path (letter case, short names) no longer counts as a different file.
|
|
45
|
+
- An ignore pattern the file system rejects, which Windows does for some
|
|
46
|
+
patterns POSIX accepts, is now skipped instead of crashing stow, unstow
|
|
47
|
+
and clean.
|
|
48
|
+
- On Windows, permission checks now read the file's ACL instead of only the
|
|
49
|
+
read-only flag, so an unreadable or unwritable path is reported before
|
|
50
|
+
anything changes rather than raising `PermissionError` part way through.
|
|
51
|
+
- Blank lines in `.dploystowignore`, or an empty `--ignore` pattern, no
|
|
52
|
+
longer crash stow/unstow/clean.
|
|
53
|
+
- Stowing a second package that shares a directory two or more levels deep
|
|
54
|
+
with an already-stowed package no longer writes into the first package's
|
|
55
|
+
source tree.
|
|
56
|
+
- Recursive ignore patterns, like `**/*.swp`, no longer cause dploy to skip
|
|
57
|
+
the whole folder a matching file lives in, including when that folder is
|
|
58
|
+
shared with another package being stowed.
|
|
59
|
+
- `unstow` no longer leaves behind an empty directory when all of its
|
|
60
|
+
contents are removed within the same run, for example a subdirectory
|
|
61
|
+
cleared while its parent directory only contained that one entry.
|
|
62
|
+
- `clean` now removes dangling links. It previously removed nothing, because
|
|
63
|
+
it could not match a link to its package unless the package sat directly in
|
|
64
|
+
the working directory and the link's target had no `..` in it. An unreadable
|
|
65
|
+
directory is now reported as an error instead of crashing.
|
|
66
|
+
- `link` now reports a clean error when the destination's parent is a file
|
|
67
|
+
instead of a directory. It previously crashed with a traceback.
|
|
68
|
+
|
|
69
|
+
## [0.1.3] - 2026-02-13
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- Added a `dploy clean` subcommand.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- Switch to Poetry for packaging instead of setuptools.
|
|
78
|
+
- Require at least Python >= 3.10.
|
|
79
|
+
- Use black for auto formatting instead of yapf.
|
|
80
|
+
- Use custom *DployError* exceptions.
|
|
81
|
+
- Version is now sourced from pyproject.toml via importlib.metadata.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- Print syntax help if no valid subcommand was provided.
|
|
86
|
+
- Fixed pytest.
|
|
87
|
+
- Fixed invoke tasks to work with Poetry and Python 3.13.
|
|
88
|
+
|
|
17
89
|
### Removed
|
|
18
90
|
|
|
91
|
+
- Removed requirements.txt in favor of pyproject.toml dependency groups.
|
|
92
|
+
|
|
19
93
|
## [0.1.2] - 2017-10-26
|
|
20
94
|
|
|
21
95
|
### Changed
|
|
@@ -43,8 +117,6 @@ organized by order of importance.
|
|
|
43
117
|
Since the symbolic links can't use a relative path fallback to the absolute
|
|
44
118
|
version of the path passed in.
|
|
45
119
|
|
|
46
|
-
### Removed
|
|
47
|
-
|
|
48
120
|
## [0.1.1] - 2016-12-29
|
|
49
121
|
|
|
50
122
|
### Fix
|
dploy-0.2.0/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|
dploy-0.2.0/CONTEXT.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# dploy
|
|
2
|
+
|
|
3
|
+
dploy creates and removes symbolic links so that the contents of one directory
|
|
4
|
+
appear inside another, in the manner of GNU Stow, on Windows, Linux, and macOS.
|
|
5
|
+
|
|
6
|
+
## Language
|
|
7
|
+
|
|
8
|
+
### What gets linked
|
|
9
|
+
|
|
10
|
+
**Package**:
|
|
11
|
+
A directory whose contents are linked into a destination as a unit. Packages are
|
|
12
|
+
always named explicitly on the command line; dploy has no directory it searches
|
|
13
|
+
for them.
|
|
14
|
+
_Avoid_: source, source directory, stow package
|
|
15
|
+
|
|
16
|
+
**Destination**:
|
|
17
|
+
The directory a package's contents are linked into.
|
|
18
|
+
_Avoid_: target, target directory, dest
|
|
19
|
+
|
|
20
|
+
**Target**:
|
|
21
|
+
The path a symbolic link points at. A link's target is what it refers to, never
|
|
22
|
+
where it lives.
|
|
23
|
+
_Avoid_: referent, linked-to path
|
|
24
|
+
|
|
25
|
+
**Dangling link**:
|
|
26
|
+
A symbolic link whose target no longer exists.
|
|
27
|
+
_Avoid_: broken link, orphaned link, stale link
|
|
28
|
+
|
|
29
|
+
### Dot-file names
|
|
30
|
+
|
|
31
|
+
**Dotfile translation**:
|
|
32
|
+
With `--dotfiles`, linking a package entry named `dot-foo` under the destination
|
|
33
|
+
name `.foo`. A name where `dot-` is followed by nothing or by a dot is never
|
|
34
|
+
translated.
|
|
35
|
+
_Avoid_: dot renaming, dot expansion
|
|
36
|
+
|
|
37
|
+
**Literal name**:
|
|
38
|
+
A package entry's own name, such as `dot-foo`.
|
|
39
|
+
_Avoid_: original name, source name
|
|
40
|
+
|
|
41
|
+
**Translated name**:
|
|
42
|
+
The name dotfile translation gives an entry, such as `.foo`. For an entry that is
|
|
43
|
+
not translated, it is the same as the literal name.
|
|
44
|
+
_Avoid_: dot name, renamed name
|
|
45
|
+
|
|
46
|
+
**Matching by target**:
|
|
47
|
+
How unstow finds the links to remove: a link is removed because its target is
|
|
48
|
+
inside the package, whichever of the entry's two names it carries. A file or link
|
|
49
|
+
that does not point into the package is never touched.
|
|
50
|
+
_Avoid_: name matching, flag matching
|
|
51
|
+
|
|
52
|
+
### Operations
|
|
53
|
+
|
|
54
|
+
**Stow**:
|
|
55
|
+
Linking the contents of a package into a destination. Stowing is idempotent:
|
|
56
|
+
stowing an already-stowed package reports the links as already in place.
|
|
57
|
+
_Avoid_: install, deploy, apply
|
|
58
|
+
|
|
59
|
+
**Unstow**:
|
|
60
|
+
Removing the links a stow produced, recognised by their target rather than by
|
|
61
|
+
their name. Also idempotent.
|
|
62
|
+
_Avoid_: uninstall, remove, undeploy
|
|
63
|
+
|
|
64
|
+
**Clean**:
|
|
65
|
+
Removing dangling links in a destination that point into a given package. Unlike
|
|
66
|
+
unstow, it acts on links whose targets are already gone.
|
|
67
|
+
_Avoid_: prune, sweep, garbage collect
|
|
68
|
+
|
|
69
|
+
**Link**:
|
|
70
|
+
Creating a single symbolic link at an exact path, rather than linking a whole
|
|
71
|
+
package's contents. Where the distinction matters, say "the link command" for
|
|
72
|
+
the operation and "a symbolic link" for the thing.
|
|
73
|
+
_Avoid_: symlink (as the name of the operation)
|
|
74
|
+
|
|
75
|
+
**Folding**:
|
|
76
|
+
Replacing a destination directory whose links all point into one package
|
|
77
|
+
directory with a single link to that directory.
|
|
78
|
+
_Avoid_: collapsing, compacting
|
|
79
|
+
|
|
80
|
+
**Unfolding**:
|
|
81
|
+
Replacing a link to a directory with a real directory containing links to that
|
|
82
|
+
directory's contents, so a second package can contribute entries alongside the
|
|
83
|
+
first.
|
|
84
|
+
_Avoid_: expanding, exploding
|
|
85
|
+
|
|
86
|
+
### How work is described
|
|
87
|
+
|
|
88
|
+
**Action**:
|
|
89
|
+
A single filesystem change dploy intends to make: creating a link, removing a
|
|
90
|
+
link, creating a directory, removing a directory. Actions are collected and
|
|
91
|
+
validated as a set before any of them run, so a command either applies fully or
|
|
92
|
+
not at all.
|
|
93
|
+
_Avoid_: operation, step, task, change
|
|
94
|
+
|
|
95
|
+
**Conflict**:
|
|
96
|
+
A reason a collected set of actions cannot be applied, such as a destination
|
|
97
|
+
entry that is not a link dploy created, or two packages claiming the same
|
|
98
|
+
destination path. Conflicts are reported together and abort the command before
|
|
99
|
+
any action runs.
|
|
100
|
+
_Avoid_: error, clash, collision
|
|
101
|
+
|
|
102
|
+
**Ignore pattern**:
|
|
103
|
+
A glob that excludes matching entries of a package from being linked. Patterns
|
|
104
|
+
come from the command line or from a `.dploystowignore` file beside the package.
|
|
105
|
+
_Avoid_: exclude pattern, skip pattern, filter
|
dploy-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dploy
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Provides functionality similar to GNU Stow as a cross platform CLI tool and Python 3 module
|
|
5
|
+
Project-URL: Homepage, https://github.com/arecarn/dploy
|
|
6
|
+
Author-email: Ryan Carney <arecarn@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE.txt
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Dploy
|
|
22
|
+
|
|
23
|
+
Dploy is a tool for creating symbolic links similarly to [GNU
|
|
24
|
+
Stow](https://www.gnu.org/software/stow/). It is provided as a CLI tool and
|
|
25
|
+
Python 3.10+ module and supports Windows, Linux, and OSX.
|
|
26
|
+
|
|
27
|
+
Dploy's command `stow` creates symbolic links to the contents of packages in a
|
|
28
|
+
specified destination directory. Repeating the
|
|
29
|
+
`stow` command with the same arguments will confirm that the contents of the
|
|
30
|
+
package have been symbolically linked.
|
|
31
|
+
|
|
32
|
+
Dploy's command `unstow` removes symbolic links that resulted from `stow`
|
|
33
|
+
commands. Repeating the `unstow` command with the same arguments will confirm
|
|
34
|
+
that the links to stowed packages have been removed.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
* Latest Release: `pip install dploy`
|
|
39
|
+
* Development Version: `pip install git+https://github.com/arecarn/dploy.git`
|
|
40
|
+
|
|
41
|
+
## Basic CLI Usage
|
|
42
|
+
|
|
43
|
+
* `dploy stow <package-directory>... <destination-directory>`
|
|
44
|
+
* `dploy unstow <package-directory>... <destination-directory>`
|
|
45
|
+
* `dploy clean <package-directory>... <destination-directory>`
|
|
46
|
+
* `dploy --help`
|
|
47
|
+
|
|
48
|
+
### Dot-file names
|
|
49
|
+
|
|
50
|
+
`stow --dotfiles` stows a package entry named `dot-something` as a destination
|
|
51
|
+
entry named `.something`:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
dploy stow --dotfiles ~/dotfiles/bash ~
|
|
55
|
+
# ~/dotfiles/bash/dot-bashrc -> ~/.bashrc
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This keeps the files visible in the package directory while they land as
|
|
59
|
+
dot-files in the destination.
|
|
60
|
+
|
|
61
|
+
`unstow` does not need `--dotfiles`. It finds links by what they point at, so it
|
|
62
|
+
removes them whether they are named `dot-something` or `.something`. It still
|
|
63
|
+
accepts the flag, for symmetry with `stow`; with it, an unrelated file already
|
|
64
|
+
named `.something` is reported as a conflict. `clean` is unaffected for the
|
|
65
|
+
same reason.
|
|
66
|
+
|
|
67
|
+
An entry whose name is `dot-` followed by nothing or by a dot (`dot-`,
|
|
68
|
+
`dot-.`, `dot-..`, `dot-.hidden`) is left untranslated, as GNU Stow does; the
|
|
69
|
+
first two would otherwise resolve to the destination directory or its parent.
|
|
70
|
+
|
|
71
|
+
## Rationale
|
|
72
|
+
|
|
73
|
+
Dploy started out as simple Python script to create symbolic links to my
|
|
74
|
+
dotfiles for Windows, Mac, and Linux. Over time I keep improving and tweaking my
|
|
75
|
+
script to suit my needs, but I was running into a problem. Keeping all the
|
|
76
|
+
files I wanted to link in a config file was becoming a real pain in the neck.
|
|
77
|
+
|
|
78
|
+
I started looking for another solution to solve my problem, and found many
|
|
79
|
+
alternatives but none of them seemed to be a good fit. The solution that seemed
|
|
80
|
+
the most promising was using GNU Stow. It seemed like the most simple elegant
|
|
81
|
+
solution to the problem. The only issue was that it didn't support Windows.
|
|
82
|
+
|
|
83
|
+
Then I thought to myself, why can't I just create my own version of Stow that
|
|
84
|
+
work on Windows, Linux and OSX. So after that my I started morphing
|
|
85
|
+
simple python script into what would become Dploy and learned a lot more about
|
|
86
|
+
python in the process.
|
|
87
|
+
|
|
88
|
+
## How does it compare with GNU Stow?
|
|
89
|
+
|
|
90
|
+
Below are just a few few major points of comparison between GNU stow and Dploy.
|
|
91
|
+
|
|
92
|
+
* Like GNU Stow Dploy runs in two passes. First by collecting the actions
|
|
93
|
+
required to complete the command and verifying that the command can
|
|
94
|
+
completed without any issues. If no issues are detected then the second
|
|
95
|
+
pass executes these actions are execute to complete the command. Otherwise
|
|
96
|
+
Dploy will exit and indicate why the command can not be completed. This way a
|
|
97
|
+
stow or unstow operation is atomic and never partially done.
|
|
98
|
+
|
|
99
|
+
* Like Stow, Dploy supports tree folding and tree unfolding. Like Stow's
|
|
100
|
+
`--no-folding`, passing `--no-folding` to `stow` or `unstow` disables
|
|
101
|
+
folding, so each file gets its own link inside a real directory.
|
|
102
|
+
|
|
103
|
+
* Unlike Stow, Dploy requires an explicit package(s) and a destination
|
|
104
|
+
directory.
|
|
105
|
+
|
|
106
|
+
* Unlike Stow, Dploy does not have any concept of ownership, but will only
|
|
107
|
+
operate on symbolic links and the creation or removal of directories for these
|
|
108
|
+
symbolic links.
|
|
109
|
+
|
|
110
|
+
* Unlike Stow, Dploy's `unstow` removes a destination directory that becomes
|
|
111
|
+
empty as a result of removing its symbolic links, even if that directory
|
|
112
|
+
existed before `stow` created any links inside it. Dploy keeps no state
|
|
113
|
+
across invocations, so it cannot distinguish a directory it created from
|
|
114
|
+
one that already existed.
|
|
115
|
+
|
|
116
|
+
* `stow`'s `--skip-conflicts` flag is the one opt-in exception to the
|
|
117
|
+
atomicity described above: files that conflict with an existing,
|
|
118
|
+
unmanaged destination entry are skipped and reported, while every other
|
|
119
|
+
file in the package is still linked. `dploy` exits with status `2` when
|
|
120
|
+
this happens, so a script can tell a partial stow apart from a full
|
|
121
|
+
success (status `0`) or a fatal error (status `1`).
|
dploy-0.2.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Dploy
|
|
2
|
+
|
|
3
|
+
Dploy is a tool for creating symbolic links similarly to [GNU
|
|
4
|
+
Stow](https://www.gnu.org/software/stow/). It is provided as a CLI tool and
|
|
5
|
+
Python 3.10+ module and supports Windows, Linux, and OSX.
|
|
6
|
+
|
|
7
|
+
Dploy's command `stow` creates symbolic links to the contents of packages in a
|
|
8
|
+
specified destination directory. Repeating the
|
|
9
|
+
`stow` command with the same arguments will confirm that the contents of the
|
|
10
|
+
package have been symbolically linked.
|
|
11
|
+
|
|
12
|
+
Dploy's command `unstow` removes symbolic links that resulted from `stow`
|
|
13
|
+
commands. Repeating the `unstow` command with the same arguments will confirm
|
|
14
|
+
that the links to stowed packages have been removed.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
* Latest Release: `pip install dploy`
|
|
19
|
+
* Development Version: `pip install git+https://github.com/arecarn/dploy.git`
|
|
20
|
+
|
|
21
|
+
## Basic CLI Usage
|
|
22
|
+
|
|
23
|
+
* `dploy stow <package-directory>... <destination-directory>`
|
|
24
|
+
* `dploy unstow <package-directory>... <destination-directory>`
|
|
25
|
+
* `dploy clean <package-directory>... <destination-directory>`
|
|
26
|
+
* `dploy --help`
|
|
27
|
+
|
|
28
|
+
### Dot-file names
|
|
29
|
+
|
|
30
|
+
`stow --dotfiles` stows a package entry named `dot-something` as a destination
|
|
31
|
+
entry named `.something`:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
dploy stow --dotfiles ~/dotfiles/bash ~
|
|
35
|
+
# ~/dotfiles/bash/dot-bashrc -> ~/.bashrc
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This keeps the files visible in the package directory while they land as
|
|
39
|
+
dot-files in the destination.
|
|
40
|
+
|
|
41
|
+
`unstow` does not need `--dotfiles`. It finds links by what they point at, so it
|
|
42
|
+
removes them whether they are named `dot-something` or `.something`. It still
|
|
43
|
+
accepts the flag, for symmetry with `stow`; with it, an unrelated file already
|
|
44
|
+
named `.something` is reported as a conflict. `clean` is unaffected for the
|
|
45
|
+
same reason.
|
|
46
|
+
|
|
47
|
+
An entry whose name is `dot-` followed by nothing or by a dot (`dot-`,
|
|
48
|
+
`dot-.`, `dot-..`, `dot-.hidden`) is left untranslated, as GNU Stow does; the
|
|
49
|
+
first two would otherwise resolve to the destination directory or its parent.
|
|
50
|
+
|
|
51
|
+
## Rationale
|
|
52
|
+
|
|
53
|
+
Dploy started out as simple Python script to create symbolic links to my
|
|
54
|
+
dotfiles for Windows, Mac, and Linux. Over time I keep improving and tweaking my
|
|
55
|
+
script to suit my needs, but I was running into a problem. Keeping all the
|
|
56
|
+
files I wanted to link in a config file was becoming a real pain in the neck.
|
|
57
|
+
|
|
58
|
+
I started looking for another solution to solve my problem, and found many
|
|
59
|
+
alternatives but none of them seemed to be a good fit. The solution that seemed
|
|
60
|
+
the most promising was using GNU Stow. It seemed like the most simple elegant
|
|
61
|
+
solution to the problem. The only issue was that it didn't support Windows.
|
|
62
|
+
|
|
63
|
+
Then I thought to myself, why can't I just create my own version of Stow that
|
|
64
|
+
work on Windows, Linux and OSX. So after that my I started morphing
|
|
65
|
+
simple python script into what would become Dploy and learned a lot more about
|
|
66
|
+
python in the process.
|
|
67
|
+
|
|
68
|
+
## How does it compare with GNU Stow?
|
|
69
|
+
|
|
70
|
+
Below are just a few few major points of comparison between GNU stow and Dploy.
|
|
71
|
+
|
|
72
|
+
* Like GNU Stow Dploy runs in two passes. First by collecting the actions
|
|
73
|
+
required to complete the command and verifying that the command can
|
|
74
|
+
completed without any issues. If no issues are detected then the second
|
|
75
|
+
pass executes these actions are execute to complete the command. Otherwise
|
|
76
|
+
Dploy will exit and indicate why the command can not be completed. This way a
|
|
77
|
+
stow or unstow operation is atomic and never partially done.
|
|
78
|
+
|
|
79
|
+
* Like Stow, Dploy supports tree folding and tree unfolding. Like Stow's
|
|
80
|
+
`--no-folding`, passing `--no-folding` to `stow` or `unstow` disables
|
|
81
|
+
folding, so each file gets its own link inside a real directory.
|
|
82
|
+
|
|
83
|
+
* Unlike Stow, Dploy requires an explicit package(s) and a destination
|
|
84
|
+
directory.
|
|
85
|
+
|
|
86
|
+
* Unlike Stow, Dploy does not have any concept of ownership, but will only
|
|
87
|
+
operate on symbolic links and the creation or removal of directories for these
|
|
88
|
+
symbolic links.
|
|
89
|
+
|
|
90
|
+
* Unlike Stow, Dploy's `unstow` removes a destination directory that becomes
|
|
91
|
+
empty as a result of removing its symbolic links, even if that directory
|
|
92
|
+
existed before `stow` created any links inside it. Dploy keeps no state
|
|
93
|
+
across invocations, so it cannot distinguish a directory it created from
|
|
94
|
+
one that already existed.
|
|
95
|
+
|
|
96
|
+
* `stow`'s `--skip-conflicts` flag is the one opt-in exception to the
|
|
97
|
+
atomicity described above: files that conflict with an existing,
|
|
98
|
+
unmanaged destination entry are skipped and reported, while every other
|
|
99
|
+
file in the package is still linked. `dploy` exits with status `2` when
|
|
100
|
+
this happens, so a script can tell a partial stow apart from a full
|
|
101
|
+
success (status `0`) or a fatal error (status `1`).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Validate permissions on Windows with an ACL check, not os.access
|
|
2
|
+
|
|
3
|
+
Status: accepted (issue #28)
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
dploy runs in two passes. The collect pass validates the whole command and the
|
|
8
|
+
execute pass applies it, so a command either completes or changes nothing. The
|
|
9
|
+
README states this as a reason to use dploy over GNU Stow.
|
|
10
|
+
|
|
11
|
+
The collect pass checks permissions with `os.access`. On Windows that call only
|
|
12
|
+
reflects the read-only attribute and ignores ACLs, so a directory the user
|
|
13
|
+
cannot read passes validation and raises `PermissionError` during execution,
|
|
14
|
+
after actions have started applying.
|
|
15
|
+
|
|
16
|
+
The guarantee is cross-platform. A check that cannot be made honest on one
|
|
17
|
+
platform narrows the guarantee there instead of being quietly excepted.
|
|
18
|
+
|
|
19
|
+
## Decision
|
|
20
|
+
|
|
21
|
+
On Windows the five permission helpers in `dploy/utils.py` ask the OS the
|
|
22
|
+
question it asks when a path is opened: Win32 `AccessCheck` over the path's
|
|
23
|
+
security descriptor, using the current user's token. POSIX keeps `os.access`.
|
|
24
|
+
|
|
25
|
+
The Win32 calls are made through `ctypes` in `dploy/_winaccess.py`, so dploy
|
|
26
|
+
keeps no runtime dependencies.
|
|
27
|
+
|
|
28
|
+
## Options considered
|
|
29
|
+
|
|
30
|
+
- **ACL check via `pywin32`.** Rejected: `pywin32` does not expose
|
|
31
|
+
`AccessCheck`. Its `GetEffectiveRightsFromAcl` ignores privileges and
|
|
32
|
+
logon-session groups, and fails on an ACL with an inherited deny entry, which
|
|
33
|
+
is the common case.
|
|
34
|
+
- **Try the operation and handle the failure.** Rejected: the collect pass
|
|
35
|
+
exists so that nothing is touched before the command is known to succeed.
|
|
36
|
+
- **Probe with a cheap reversible access attempt.** Rejected: it makes the
|
|
37
|
+
collect pass write to the filesystem, and still races with the execute pass.
|
|
38
|
+
- **Accept and document the gap.** Rejected: it makes the atomicity claim
|
|
39
|
+
conditional on the platform.
|
|
40
|
+
|
|
41
|
+
## Consequences
|
|
42
|
+
|
|
43
|
+
- The Windows permission code is one `ctypes` module that only `windows-latest`
|
|
44
|
+
CI exercises. Wrong struct layouts fail there, not on Linux. Tests in
|
|
45
|
+
`tests/test_utils.py` deny access with real ACL entries through `icacls`.
|
|
46
|
+
- The check is a point-in-time answer. A permission changed between the two
|
|
47
|
+
passes can still fail execution, on every platform.
|
|
48
|
+
- A path that cannot be examined (missing, bad name, no ACL support) counts as
|
|
49
|
+
not accessible, as with `os.access`. Failures of the token or `AccessCheck`
|
|
50
|
+
calls raise `OSError`, because the answer is then unknown rather than "no".
|
|
51
|
+
- A writable directory needs both the add-file and the add-subdirectory right,
|
|
52
|
+
because stow creates files, directories and directory links. Removing an
|
|
53
|
+
entry is not checked: it depends on `DELETE` on the entry itself, which the
|
|
54
|
+
parent's ACL does not show.
|
|
55
|
+
- The execute (traverse) check is stricter than the OS. Users hold the
|
|
56
|
+
"bypass traverse checking" privilege, which `AccessCheck` does not model, so
|
|
57
|
+
the OS reaches through a directory whose traverse right is denied. dploy
|
|
58
|
+
refuses it instead, because the ACL says no. A refusal in the collect pass
|
|
59
|
+
never breaks the all-or-nothing promise; only a failure in the execute pass
|
|
60
|
+
would.
|
|
61
|
+
- The permission tests run on Windows. Their `remove_*_permission` helpers set
|
|
62
|
+
a deny entry with `icacls` there, and teardown clears them first. Three
|
|
63
|
+
tests stay skipped because they need the OS itself to refuse traversal,
|
|
64
|
+
which the bypass above prevents (`skip_on_windows_traverse_bypass`).
|
|
65
|
+
- Each check opens and duplicates a process token. This is not cached because
|
|
66
|
+
the cost is small next to the file system calls; cache it if profiling says
|
|
67
|
+
otherwise.
|