secflow-cli 0.2.5__tar.gz → 0.3.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.
- {secflow_cli-0.2.5/src/secflow_cli.egg-info → secflow_cli-0.3.0}/PKG-INFO +11 -10
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/USAGE.md +9 -9
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/pyproject.toml +3 -2
- secflow_cli-0.3.0/src/secflow_cli/__init__.py +1 -0
- secflow_cli-0.3.0/src/secflow_cli/__main__.py +3 -0
- secflow_cli-0.3.0/src/secflow_cli/bootstrap.py +37 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/main.py +3 -2
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/output.py +4 -0
- secflow_cli-0.3.0/src/secflow_cli/update_install.py +322 -0
- secflow_cli-0.3.0/src/secflow_cli/update_runtime.py +511 -0
- secflow_cli-0.3.0/src/secflow_cli/update_security.py +170 -0
- secflow_cli-0.3.0/src/secflow_cli/update_state.py +195 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0/src/secflow_cli.egg-info}/PKG-INFO +11 -10
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/SOURCES.txt +5 -0
- secflow_cli-0.3.0/src/secflow_cli.egg-info/entry_points.txt +2 -0
- secflow_cli-0.2.5/src/secflow_cli/__init__.py +0 -0
- secflow_cli-0.2.5/src/secflow_cli/__main__.py +0 -3
- secflow_cli-0.2.5/src/secflow_cli.egg-info/entry_points.txt +0 -2
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/LICENSE +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/setup.cfg +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/access_profile_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/account_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/browser_login.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/command_support.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/completion.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/config.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/credentials.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/doctor_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/errors.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/help.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/logkit/__init__.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/logkit/test_logkit.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/manifest_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/namespace_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/operator_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/paging.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/policy_audit_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/reference_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/report_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/role_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/runtime_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/secret_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/session_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/token_commands.py +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/dependency_links.txt +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/requires.txt +0 -0
- {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secflow-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: SecFlow operator CLI
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/yeon3724/secflow
|
|
7
7
|
Project-URL: Issues, https://github.com/yeon3724/secflow/issues
|
|
8
|
+
Keywords: intflow-cli-update-protocol-1
|
|
8
9
|
Requires-Python: >=3.10
|
|
9
10
|
Description-Content-Type: text/markdown
|
|
10
11
|
License-File: LICENSE
|
|
@@ -14,11 +15,11 @@ Dynamic: license-file
|
|
|
14
15
|
|
|
15
16
|
# SecFlow CLI
|
|
16
17
|
|
|
17
|
-
Use Intflow browser login or an
|
|
18
|
+
Use Intflow browser login or an Admin session token.
|
|
18
19
|
|
|
19
20
|
## Install and update
|
|
20
21
|
|
|
21
|
-
Choose
|
|
22
|
+
Choose npm or Python; both own `secflowctl` and share profiles and OS credentials.
|
|
22
23
|
|
|
23
24
|
```sh
|
|
24
25
|
pipx install secflow-cli
|
|
@@ -30,19 +31,19 @@ npm install --global @intflow/secflow-cli
|
|
|
30
31
|
secflowctl --version
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
Python 3.10+ is required; installation includes the pinned Admin-capable SDK.
|
|
34
35
|
|
|
35
|
-
The npm distribution
|
|
36
|
+
The npm distribution bundles Python and dependencies for Windows x64, macOS Intel/Apple Silicon, and Linux x64 glibc 2.35+. Alpine/musl and Linux ARM use the Python distribution. Keep optional dependencies enabled; installation has no postinstall script. Releases require all four same-version native packages. Binaries are not platform-signed or notarized.
|
|
36
37
|
|
|
37
|
-
The
|
|
38
|
+
The bundle includes HTTPS CAs; `SSL_CERT_FILE`/`SSL_CERT_DIR` overrides trust without disabling TLS verification.
|
|
38
39
|
|
|
39
40
|
```sh
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
python -m pip install --upgrade secflow-cli
|
|
43
|
-
npm install --global @intflow/secflow-cli@latest
|
|
41
|
+
secflowctl update --check
|
|
42
|
+
secflowctl update --status
|
|
44
43
|
```
|
|
45
44
|
|
|
45
|
+
Commands check within 3s and stage protocol-1 stable runtimes in a private cache/venv, then run argv once; offline failures keep the last good runtime. TTY/JSON agree; help/version/check/status never install. `update --apply` installs; `--auto on|off` persists policy; `--pin VERSION` freezes an exact runtime; `--unpin` releases only policy. `INTFLOW_CLI_AUTO_UPDATE=0` stops automatic installs, allowing explicit apply/pin. Pins outrank off and fail closed. Internal native binaries update through npm.
|
|
46
|
+
|
|
46
47
|
## Connect
|
|
47
48
|
|
|
48
49
|
```sh
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# SecFlow CLI
|
|
2
2
|
|
|
3
|
-
Use Intflow browser login or an
|
|
3
|
+
Use Intflow browser login or an Admin session token.
|
|
4
4
|
|
|
5
5
|
## Install and update
|
|
6
6
|
|
|
7
|
-
Choose
|
|
7
|
+
Choose npm or Python; both own `secflowctl` and share profiles and OS credentials.
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
pipx install secflow-cli
|
|
@@ -16,19 +16,19 @@ npm install --global @intflow/secflow-cli
|
|
|
16
16
|
secflowctl --version
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Python 3.10+ is required; installation includes the pinned Admin-capable SDK.
|
|
20
20
|
|
|
21
|
-
The npm distribution
|
|
21
|
+
The npm distribution bundles Python and dependencies for Windows x64, macOS Intel/Apple Silicon, and Linux x64 glibc 2.35+. Alpine/musl and Linux ARM use the Python distribution. Keep optional dependencies enabled; installation has no postinstall script. Releases require all four same-version native packages. Binaries are not platform-signed or notarized.
|
|
22
22
|
|
|
23
|
-
The
|
|
23
|
+
The bundle includes HTTPS CAs; `SSL_CERT_FILE`/`SSL_CERT_DIR` overrides trust without disabling TLS verification.
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
python -m pip install --upgrade secflow-cli
|
|
29
|
-
npm install --global @intflow/secflow-cli@latest
|
|
26
|
+
secflowctl update --check
|
|
27
|
+
secflowctl update --status
|
|
30
28
|
```
|
|
31
29
|
|
|
30
|
+
Commands check within 3s and stage protocol-1 stable runtimes in a private cache/venv, then run argv once; offline failures keep the last good runtime. TTY/JSON agree; help/version/check/status never install. `update --apply` installs; `--auto on|off` persists policy; `--pin VERSION` freezes an exact runtime; `--unpin` releases only policy. `INTFLOW_CLI_AUTO_UPDATE=0` stops automatic installs, allowing explicit apply/pin. Pins outrank off and fail closed. Internal native binaries update through npm.
|
|
31
|
+
|
|
32
32
|
## Connect
|
|
33
33
|
|
|
34
34
|
```sh
|
|
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "secflow-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "SecFlow operator CLI"
|
|
9
9
|
readme = "USAGE.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["intflow-cli-update-protocol-1"]
|
|
12
13
|
dependencies = ["secflow-sdk==0.1.10", "keyring>=25,<26"]
|
|
13
14
|
|
|
14
15
|
[project.scripts]
|
|
15
|
-
secflowctl = "secflow_cli.
|
|
16
|
+
secflowctl = "secflow_cli.bootstrap:main"
|
|
16
17
|
|
|
17
18
|
[project.urls]
|
|
18
19
|
Repository = "https://github.com/yeon3724/secflow"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from . import __version__
|
|
7
|
+
from .update_runtime import bootstrap
|
|
8
|
+
from .update_state import Configuration, default_root
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def execute(arguments: list[str]) -> int:
|
|
12
|
+
from .main import main
|
|
13
|
+
|
|
14
|
+
return main(arguments, configure=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def output(value: dict[str, Any]) -> None:
|
|
18
|
+
from .output import update_result
|
|
19
|
+
|
|
20
|
+
update_result(value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(argv: list[str] | None = None) -> int:
|
|
24
|
+
config = Configuration(
|
|
25
|
+
"secflow-cli", "secflow_cli", __version__, default_root("secflow-cli")
|
|
26
|
+
)
|
|
27
|
+
return bootstrap(
|
|
28
|
+
config,
|
|
29
|
+
list(sys.argv[1:] if argv is None else argv),
|
|
30
|
+
execute,
|
|
31
|
+
output,
|
|
32
|
+
frozen=bool(getattr(sys, "frozen", False)),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
raise SystemExit(main())
|
|
@@ -53,6 +53,7 @@ class VersionAction(argparse.Action):
|
|
|
53
53
|
|
|
54
54
|
def parser():
|
|
55
55
|
result = Parser(prog="secflowctl", description="SecFlow operator CLI (independent of the service CLI).")
|
|
56
|
+
result.epilog = "CLI updates: update [--check|--apply|--status|--auto on|off|--pin VERSION|--unpin]."
|
|
56
57
|
result.add_argument("--version", action=VersionAction, nargs=0, help="Show the CLI version and exit.")
|
|
57
58
|
result.add_argument("--profile")
|
|
58
59
|
result.add_argument("--base-url")
|
|
@@ -496,8 +497,8 @@ def dispatch(arguments, run):
|
|
|
496
497
|
return run_configured(args, run, quick_login)
|
|
497
498
|
|
|
498
499
|
|
|
499
|
-
def main(argv=None):
|
|
500
|
-
if argv is None:
|
|
500
|
+
def main(argv=None, *, configure=False):
|
|
501
|
+
if argv is None or configure:
|
|
501
502
|
logkit.configure("secflowctl", kind="cli")
|
|
502
503
|
run = Invocation("table" if sys.stdout.isatty() else "json")
|
|
503
504
|
try:
|
|
@@ -14,6 +14,10 @@ TOKEN_FIELDS = ("token_id", "account_id", "scope_type", "scope", "issued_at", "e
|
|
|
14
14
|
ACCOUNT_ROLES = ("org_admin", "namespace_admin", "secret_writer", "secret_reader")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def update_result(value):
|
|
18
|
+
print(json.dumps(value, ensure_ascii=True, separators=(",", ":")))
|
|
19
|
+
|
|
20
|
+
|
|
17
21
|
def safe_fields(value, fields):
|
|
18
22
|
if not isinstance(value, Mapping):
|
|
19
23
|
raise CliError(7, "INVALID_RESPONSE", "The server returned an invalid metadata response.")
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import http.client
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import queue
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .update_security import directory, windows_tree
|
|
18
|
+
from .update_state import (
|
|
19
|
+
CHILD,
|
|
20
|
+
MARKER,
|
|
21
|
+
PROTOCOL,
|
|
22
|
+
Configuration,
|
|
23
|
+
UpdateError,
|
|
24
|
+
checked,
|
|
25
|
+
digest,
|
|
26
|
+
executable,
|
|
27
|
+
private,
|
|
28
|
+
read,
|
|
29
|
+
release_path,
|
|
30
|
+
remove_release,
|
|
31
|
+
save,
|
|
32
|
+
version,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def compatible(specification: Any) -> bool:
|
|
37
|
+
if not isinstance(specification, str) or not specification:
|
|
38
|
+
return False
|
|
39
|
+
current = sys.version_info[:3]
|
|
40
|
+
operations = {
|
|
41
|
+
">=": lambda wanted: current >= wanted,
|
|
42
|
+
">": lambda wanted: current > wanted,
|
|
43
|
+
"<=": lambda wanted: current <= wanted,
|
|
44
|
+
"<": lambda wanted: current < wanted,
|
|
45
|
+
"==": lambda wanted: current == wanted,
|
|
46
|
+
"!=": lambda wanted: current != wanted,
|
|
47
|
+
}
|
|
48
|
+
for term in specification.split(","):
|
|
49
|
+
match = re.fullmatch(
|
|
50
|
+
r"\s*(>=|<=|==|!=|>|<)\s*(\d+)\.(\d+)(?:\.(\d+))?\s*", term
|
|
51
|
+
)
|
|
52
|
+
if match is None:
|
|
53
|
+
return False
|
|
54
|
+
wanted = tuple(int(value or 0) for value in match.groups()[1:])
|
|
55
|
+
if not operations[match[1]](wanted):
|
|
56
|
+
return False
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def registry_bytes(url: str) -> bytes:
|
|
61
|
+
target = urllib.parse.urlsplit(url)
|
|
62
|
+
if (
|
|
63
|
+
target.scheme != "https"
|
|
64
|
+
or target.netloc != "pypi.org"
|
|
65
|
+
or target.query
|
|
66
|
+
or target.fragment
|
|
67
|
+
):
|
|
68
|
+
raise UpdateError("INVALID_REGISTRY_RESPONSE")
|
|
69
|
+
connection = http.client.HTTPSConnection("pypi.org", timeout=3)
|
|
70
|
+
try:
|
|
71
|
+
connection.request("GET", target.path, headers={"Accept": "application/json"})
|
|
72
|
+
response = connection.getresponse()
|
|
73
|
+
if response.status != 200:
|
|
74
|
+
raise UpdateError("REGISTRY_UNAVAILABLE")
|
|
75
|
+
content = response.read(1048577)
|
|
76
|
+
finally:
|
|
77
|
+
connection.close()
|
|
78
|
+
if len(content) > 1048576:
|
|
79
|
+
raise UpdateError("INVALID_REGISTRY_RESPONSE")
|
|
80
|
+
return content
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def registry_document(url: str) -> dict[str, Any]:
|
|
84
|
+
result: queue.Queue[bytes | Exception] = queue.Queue(maxsize=1)
|
|
85
|
+
|
|
86
|
+
def fetch() -> None:
|
|
87
|
+
try:
|
|
88
|
+
result.put(registry_bytes(url))
|
|
89
|
+
except (OSError, ValueError, http.client.HTTPException, UpdateError) as error:
|
|
90
|
+
result.put(error)
|
|
91
|
+
|
|
92
|
+
threading.Thread(target=fetch, daemon=True).start()
|
|
93
|
+
try:
|
|
94
|
+
content = result.get(timeout=3)
|
|
95
|
+
except queue.Empty:
|
|
96
|
+
raise UpdateError("REGISTRY_TIMEOUT") from None
|
|
97
|
+
if isinstance(content, Exception):
|
|
98
|
+
raise UpdateError("REGISTRY_UNAVAILABLE") from None
|
|
99
|
+
value = json.loads(content)
|
|
100
|
+
if not isinstance(value, dict) or not isinstance(value.get("info"), dict):
|
|
101
|
+
raise UpdateError("INVALID_REGISTRY_RESPONSE")
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def metadata(config: Configuration, target: str | None = None) -> dict[str, Any]:
|
|
106
|
+
if target is not None:
|
|
107
|
+
version(target)
|
|
108
|
+
suffix = "/" + target if target else ""
|
|
109
|
+
url = (
|
|
110
|
+
"https://pypi.org/pypi/"
|
|
111
|
+
+ urllib.parse.quote(config.package, safe="")
|
|
112
|
+
+ suffix
|
|
113
|
+
+ "/json"
|
|
114
|
+
)
|
|
115
|
+
value = registry_document(url)
|
|
116
|
+
info = value.get("info", {})
|
|
117
|
+
actual = info.get("version")
|
|
118
|
+
version(actual)
|
|
119
|
+
normalized = re.sub(r"[-_.]+", "-", str(info.get("name", ""))).lower()
|
|
120
|
+
if normalized != config.package or (target is not None and actual != target):
|
|
121
|
+
raise UpdateError("INVALID_REGISTRY_RESPONSE")
|
|
122
|
+
keywords = info.get("keywords")
|
|
123
|
+
if not isinstance(keywords, str) or MARKER not in re.split(r"[\s,]+", keywords):
|
|
124
|
+
raise UpdateError("UNSUPPORTED_PROTOCOL", actual)
|
|
125
|
+
if not compatible(info.get("requires_python")):
|
|
126
|
+
raise UpdateError("INCOMPATIBLE_RUNTIME", actual)
|
|
127
|
+
wheels = [
|
|
128
|
+
row
|
|
129
|
+
for row in value.get("urls", [])
|
|
130
|
+
if isinstance(row, dict)
|
|
131
|
+
and row.get("packagetype") == "bdist_wheel"
|
|
132
|
+
and row.get("yanked") is False
|
|
133
|
+
]
|
|
134
|
+
if not wheels:
|
|
135
|
+
raise UpdateError("WHEEL_UNAVAILABLE", actual)
|
|
136
|
+
return {"version": actual, "package": config.package, "protocol": PROTOCOL}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def installer_environment() -> dict[str, str]:
|
|
140
|
+
allowed = {
|
|
141
|
+
"PATH",
|
|
142
|
+
"PATHEXT",
|
|
143
|
+
"SYSTEMROOT",
|
|
144
|
+
"WINDIR",
|
|
145
|
+
"COMSPEC",
|
|
146
|
+
"HOME",
|
|
147
|
+
"USERPROFILE",
|
|
148
|
+
"LOCALAPPDATA",
|
|
149
|
+
"APPDATA",
|
|
150
|
+
"TEMP",
|
|
151
|
+
"TMP",
|
|
152
|
+
"TMPDIR",
|
|
153
|
+
"LANG",
|
|
154
|
+
"LC_ALL",
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
**{key: value for key, value in os.environ.items() if key.upper() in allowed},
|
|
158
|
+
CHILD: "1",
|
|
159
|
+
"INTFLOW_CLI_AUTO_UPDATE": "0",
|
|
160
|
+
"PYTHONUTF8": "1",
|
|
161
|
+
"PYTHONDONTWRITEBYTECODE": "1",
|
|
162
|
+
"PIP_CONFIG_FILE": os.devnull,
|
|
163
|
+
"NETRC": os.devnull,
|
|
164
|
+
"PIP_KEYRING_PROVIDER": "disabled",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def execute(
|
|
169
|
+
command: list[str], folder: Path, deadline: float, capture: bool = False
|
|
170
|
+
) -> str:
|
|
171
|
+
remaining = deadline - time.monotonic()
|
|
172
|
+
if remaining <= 0:
|
|
173
|
+
raise UpdateError("INSTALL_TIMEOUT")
|
|
174
|
+
result = subprocess.run(
|
|
175
|
+
command,
|
|
176
|
+
cwd=folder,
|
|
177
|
+
stdin=subprocess.DEVNULL,
|
|
178
|
+
stdout=subprocess.PIPE if capture else subprocess.DEVNULL,
|
|
179
|
+
stderr=subprocess.DEVNULL,
|
|
180
|
+
env=installer_environment(),
|
|
181
|
+
timeout=remaining,
|
|
182
|
+
check=False,
|
|
183
|
+
)
|
|
184
|
+
if result.returncode != 0:
|
|
185
|
+
raise UpdateError("VALIDATION_FAILED" if capture else "INSTALL_FAILED")
|
|
186
|
+
if capture:
|
|
187
|
+
if len(result.stdout) > 4096:
|
|
188
|
+
raise UpdateError("INVALID_PROBE")
|
|
189
|
+
return result.stdout.decode("utf-8").strip()
|
|
190
|
+
return ""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def site_packages(release: Path) -> Path:
|
|
194
|
+
return release / (
|
|
195
|
+
"Lib/site-packages"
|
|
196
|
+
if os.name == "nt"
|
|
197
|
+
else f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def seal_files(release: Path) -> dict[str, str]:
|
|
202
|
+
files = [executable(release), release / "pyvenv.cfg"]
|
|
203
|
+
site = checked(site_packages(release))
|
|
204
|
+
for path in site.rglob("*"):
|
|
205
|
+
checked(path)
|
|
206
|
+
if path.is_file():
|
|
207
|
+
files.append(path)
|
|
208
|
+
if len(files) > 8192 or sum(path.stat().st_size for path in files) > 134217728:
|
|
209
|
+
raise UpdateError("RELEASE_TOO_LARGE")
|
|
210
|
+
return {path.relative_to(release).as_posix(): digest(path) for path in files}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def validate(config: Configuration, pointer: dict[str, Any]) -> Path:
|
|
214
|
+
directory(config.root)
|
|
215
|
+
version(pointer.get("version"))
|
|
216
|
+
release = release_path(config.root, pointer.get("release"))
|
|
217
|
+
directory(release)
|
|
218
|
+
windows_tree(release)
|
|
219
|
+
seal = read(release, "release.json", 1048576)
|
|
220
|
+
if (
|
|
221
|
+
seal.get("package") != config.package
|
|
222
|
+
or seal.get("version") != pointer["version"]
|
|
223
|
+
or seal.get("protocol") != PROTOCOL
|
|
224
|
+
):
|
|
225
|
+
raise UpdateError("INVALID_RELEASE")
|
|
226
|
+
if seal.get("files") != seal_files(release):
|
|
227
|
+
raise UpdateError("RELEASE_TAMPERED")
|
|
228
|
+
probe(config, release, pointer["version"], time.monotonic() + 10)
|
|
229
|
+
return executable(release)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def probe(config: Configuration, release: Path, target: str, deadline: float) -> None:
|
|
233
|
+
python = str(executable(release))
|
|
234
|
+
query = (
|
|
235
|
+
"import importlib.metadata,json,sys; m=importlib.metadata.metadata(sys.argv[1]);"
|
|
236
|
+
"print(json.dumps({k:m.get(k) for k in "
|
|
237
|
+
"['Name','Version','Requires-Python','Keywords']}))"
|
|
238
|
+
)
|
|
239
|
+
installed = json.loads(
|
|
240
|
+
execute(
|
|
241
|
+
[python, "-I", "-B", "-c", query, config.package], release, deadline, True
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
normalized = re.sub(r"[-_.]+", "-", str(installed.get("Name", ""))).lower()
|
|
245
|
+
if normalized != config.package or installed.get("Version") != target:
|
|
246
|
+
raise UpdateError("VERSION_MISMATCH")
|
|
247
|
+
if MARKER not in re.split(
|
|
248
|
+
r"[\s,]+", str(installed.get("Keywords", ""))
|
|
249
|
+
) or not compatible(installed.get("Requires-Python")):
|
|
250
|
+
raise UpdateError("UNSUPPORTED_PROTOCOL")
|
|
251
|
+
payload = execute(
|
|
252
|
+
[python, "-I", "-B", "-m", config.module + ".bootstrap", "--runtime-probe"],
|
|
253
|
+
release,
|
|
254
|
+
deadline,
|
|
255
|
+
True,
|
|
256
|
+
)
|
|
257
|
+
if json.loads(payload) != {
|
|
258
|
+
"protocol": PROTOCOL,
|
|
259
|
+
"package": config.package,
|
|
260
|
+
"version": target,
|
|
261
|
+
"runtime": "python",
|
|
262
|
+
}:
|
|
263
|
+
raise UpdateError("INVALID_PROBE")
|
|
264
|
+
displayed = execute(
|
|
265
|
+
[python, "-I", "-B", "-m", config.module + ".bootstrap", "--version"],
|
|
266
|
+
release,
|
|
267
|
+
deadline,
|
|
268
|
+
True,
|
|
269
|
+
)
|
|
270
|
+
if target not in displayed.split():
|
|
271
|
+
raise UpdateError("VERSION_MISMATCH")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def install(config: Configuration, target: str) -> dict[str, Any]:
|
|
275
|
+
version(target)
|
|
276
|
+
private(config.root)
|
|
277
|
+
release = config.root / ("release-" + str(uuid.uuid4()))
|
|
278
|
+
private(release)
|
|
279
|
+
deadline = time.monotonic() + 180
|
|
280
|
+
complete = False
|
|
281
|
+
try:
|
|
282
|
+
execute(
|
|
283
|
+
[sys.executable, "-I", "-m", "venv", "--copies", str(release)],
|
|
284
|
+
release,
|
|
285
|
+
deadline,
|
|
286
|
+
)
|
|
287
|
+
execute(
|
|
288
|
+
[
|
|
289
|
+
str(executable(release)),
|
|
290
|
+
"-I",
|
|
291
|
+
"-m",
|
|
292
|
+
"pip",
|
|
293
|
+
"install",
|
|
294
|
+
"--disable-pip-version-check",
|
|
295
|
+
"--no-input",
|
|
296
|
+
"--no-cache-dir",
|
|
297
|
+
"--no-compile",
|
|
298
|
+
"--index-url",
|
|
299
|
+
"https://pypi.org/simple",
|
|
300
|
+
"--only-binary",
|
|
301
|
+
":all:",
|
|
302
|
+
config.package + "==" + target,
|
|
303
|
+
],
|
|
304
|
+
release,
|
|
305
|
+
deadline,
|
|
306
|
+
)
|
|
307
|
+
probe(config, release, target, deadline)
|
|
308
|
+
save(
|
|
309
|
+
release,
|
|
310
|
+
"release.json",
|
|
311
|
+
{
|
|
312
|
+
"package": config.package,
|
|
313
|
+
"version": target,
|
|
314
|
+
"protocol": PROTOCOL,
|
|
315
|
+
"files": seal_files(release),
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
complete = True
|
|
319
|
+
return {"version": target, "release": release.name}
|
|
320
|
+
finally:
|
|
321
|
+
if not complete:
|
|
322
|
+
remove_release(config.root, release)
|