cpyte-cpm 1.1.1__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.
- cpyte_cpm-1.1.1/PKG-INFO +145 -0
- cpyte_cpm-1.1.1/README.md +130 -0
- cpyte_cpm-1.1.1/cpyte/__init__.py +1 -0
- cpyte_cpm-1.1.1/cpyte/cli/__init__.py +0 -0
- cpyte_cpm-1.1.1/cpyte/cli/commands.py +108 -0
- cpyte_cpm-1.1.1/cpyte/cli/errors.py +49 -0
- cpyte_cpm-1.1.1/cpyte/cli/executor.py +263 -0
- cpyte_cpm-1.1.1/cpyte/cli/gethins.py +36 -0
- cpyte_cpm-1.1.1/cpyte/cli/lockfile.py +183 -0
- cpyte_cpm-1.1.1/cpyte/cli/main.py +84 -0
- cpyte_cpm-1.1.1/cpyte/cli/manifest.py +363 -0
- cpyte_cpm-1.1.1/cpyte/cli/parser.py +336 -0
- cpyte_cpm-1.1.1/cpyte/cli/sat.py +315 -0
- cpyte_cpm-1.1.1/cpyte/cli/things.py +720 -0
- cpyte_cpm-1.1.1/cpyte_cpm.egg-info/PKG-INFO +145 -0
- cpyte_cpm-1.1.1/cpyte_cpm.egg-info/SOURCES.txt +21 -0
- cpyte_cpm-1.1.1/cpyte_cpm.egg-info/dependency_links.txt +1 -0
- cpyte_cpm-1.1.1/cpyte_cpm.egg-info/entry_points.txt +2 -0
- cpyte_cpm-1.1.1/cpyte_cpm.egg-info/requires.txt +6 -0
- cpyte_cpm-1.1.1/cpyte_cpm.egg-info/top_level.txt +1 -0
- cpyte_cpm-1.1.1/pyproject.toml +38 -0
- cpyte_cpm-1.1.1/setup.cfg +4 -0
- cpyte_cpm-1.1.1/tests/test_parser.py +310 -0
cpyte_cpm-1.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cpyte-cpm
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: A package manager for cpy
|
|
5
|
+
Author: Albert
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitea.5gnew.io.vn/Cpyte-Project/Cpyte
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: requests>=2.34.2
|
|
11
|
+
Requires-Dist: cpyte
|
|
12
|
+
Requires-Dist: packaging
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# CPM — Cpyte Package Manager
|
|
17
|
+
|
|
18
|
+
A compiler-pipeline dependency resolver and installer for the [Cpyte](https://gitea.5gnew.io.vn/Cpyte-Project/Cpyte) programming language.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
import @std.json
|
|
22
|
+
↓
|
|
23
|
+
Resolve → Lower → Optimize → Execute
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install cpm
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cpm init # create cpytoml
|
|
36
|
+
cpm add @std/json@^2.0 # add a dependency
|
|
37
|
+
cpm install # install all from manifest
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|---------|-------------|
|
|
44
|
+
| `cpm init` | Initialize a new project (`cpytoml`) |
|
|
45
|
+
| `cpm add <pkg>` | Add packages to manifest + install |
|
|
46
|
+
| `cpm remove <pkg>` | Remove packages + uninstall |
|
|
47
|
+
| `cpm install` | Install all dependencies from manifest |
|
|
48
|
+
| `cpm install <pkg>` | Install specific packages |
|
|
49
|
+
| `cpm update` | Re-resolve and update all packages |
|
|
50
|
+
| `cpm update <pkg>` | Re-resolve specific packages |
|
|
51
|
+
| `cpm build` | Build the project |
|
|
52
|
+
| `cpm run <script>` | Run a named script |
|
|
53
|
+
|
|
54
|
+
## Global Flags
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
-v, --verbose Verbose output
|
|
58
|
+
-q, --quiet Suppress output
|
|
59
|
+
-y, --yes Auto-confirm prompts
|
|
60
|
+
--offline Offline mode (skip network)
|
|
61
|
+
--no-cache Disable cache
|
|
62
|
+
--config <path> Custom config / repo URL
|
|
63
|
+
--target <platform> Target platform (e.g. linux/x86_64)
|
|
64
|
+
--llvm-version <V> LLVM version for prebuilt packages
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Manifest (`cpytoml`)
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[cpm]
|
|
71
|
+
name = "my-app"
|
|
72
|
+
version = "1.0"
|
|
73
|
+
prebuilt = false
|
|
74
|
+
llvm_version = "18.1.0"
|
|
75
|
+
|
|
76
|
+
[cpm.target]
|
|
77
|
+
os = "linux"
|
|
78
|
+
arch = "x86_64"
|
|
79
|
+
features = ["gui", "ssl"]
|
|
80
|
+
|
|
81
|
+
[cpm.dependencies]
|
|
82
|
+
"@std/json" = "^2.0"
|
|
83
|
+
"@std/http" = "1.0"
|
|
84
|
+
"package_a" = "latest"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Claims System
|
|
88
|
+
|
|
89
|
+
Packages declare platform support; CPM filters based on your target.
|
|
90
|
+
|
|
91
|
+
**Package metadata (registry):**
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"name": "win32-api",
|
|
95
|
+
"claims": { "os": ["windows"], "arch": ["x86_64"] }
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Resolution:**
|
|
100
|
+
| Target | `win32-api` (windows) | `posix-api` (linux) | `@std/json` (any) |
|
|
101
|
+
|--------|----------------------|--------------------|--------------------|
|
|
102
|
+
| `linux/x86_64` | skip | install | install |
|
|
103
|
+
| `windows/x86_64` | install | skip | install |
|
|
104
|
+
|
|
105
|
+
Auto-detects from current platform when no target is specified.
|
|
106
|
+
|
|
107
|
+
## Prebuilt Mode
|
|
108
|
+
|
|
109
|
+
When the registry serves precompiled LLVM IR:
|
|
110
|
+
|
|
111
|
+
```toml
|
|
112
|
+
[cpm]
|
|
113
|
+
prebuilt = true
|
|
114
|
+
llvm_version = "18.1.0"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
CPM fetches `.ll` artifacts from `metadata/prebuilt/...` and skips source packages
|
|
118
|
+
with incompatible LLVM versions (major version must match).
|
|
119
|
+
|
|
120
|
+
## Lockfile (`cpm.lock`)
|
|
121
|
+
|
|
122
|
+
Auto-generated. Pins exact versions for reproducible builds.
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[[package]]
|
|
126
|
+
name = "@std/json"
|
|
127
|
+
version = "2.0.3"
|
|
128
|
+
resolved = "https://repo.example.com/group/std/json/2.0.3.tar.gz"
|
|
129
|
+
checksum = "sha256:abc123..."
|
|
130
|
+
dependencies = ["@std/encoding@1.2.0"]
|
|
131
|
+
llvm_version = "18.1.0"
|
|
132
|
+
cpyte_version = "0.5.0"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Directory Layout
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
~/.cpm/
|
|
139
|
+
├── cache/<name>/<version>/ # downloaded archives
|
|
140
|
+
└── modules/<name>/<version>/ # extracted packages
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# CPM — Cpyte Package Manager
|
|
2
|
+
|
|
3
|
+
A compiler-pipeline dependency resolver and installer for the [Cpyte](https://gitea.5gnew.io.vn/Cpyte-Project/Cpyte) programming language.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
import @std.json
|
|
7
|
+
↓
|
|
8
|
+
Resolve → Lower → Optimize → Execute
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install cpm
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cpm init # create cpytoml
|
|
21
|
+
cpm add @std/json@^2.0 # add a dependency
|
|
22
|
+
cpm install # install all from manifest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
| Command | Description |
|
|
28
|
+
|---------|-------------|
|
|
29
|
+
| `cpm init` | Initialize a new project (`cpytoml`) |
|
|
30
|
+
| `cpm add <pkg>` | Add packages to manifest + install |
|
|
31
|
+
| `cpm remove <pkg>` | Remove packages + uninstall |
|
|
32
|
+
| `cpm install` | Install all dependencies from manifest |
|
|
33
|
+
| `cpm install <pkg>` | Install specific packages |
|
|
34
|
+
| `cpm update` | Re-resolve and update all packages |
|
|
35
|
+
| `cpm update <pkg>` | Re-resolve specific packages |
|
|
36
|
+
| `cpm build` | Build the project |
|
|
37
|
+
| `cpm run <script>` | Run a named script |
|
|
38
|
+
|
|
39
|
+
## Global Flags
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
-v, --verbose Verbose output
|
|
43
|
+
-q, --quiet Suppress output
|
|
44
|
+
-y, --yes Auto-confirm prompts
|
|
45
|
+
--offline Offline mode (skip network)
|
|
46
|
+
--no-cache Disable cache
|
|
47
|
+
--config <path> Custom config / repo URL
|
|
48
|
+
--target <platform> Target platform (e.g. linux/x86_64)
|
|
49
|
+
--llvm-version <V> LLVM version for prebuilt packages
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Manifest (`cpytoml`)
|
|
53
|
+
|
|
54
|
+
```toml
|
|
55
|
+
[cpm]
|
|
56
|
+
name = "my-app"
|
|
57
|
+
version = "1.0"
|
|
58
|
+
prebuilt = false
|
|
59
|
+
llvm_version = "18.1.0"
|
|
60
|
+
|
|
61
|
+
[cpm.target]
|
|
62
|
+
os = "linux"
|
|
63
|
+
arch = "x86_64"
|
|
64
|
+
features = ["gui", "ssl"]
|
|
65
|
+
|
|
66
|
+
[cpm.dependencies]
|
|
67
|
+
"@std/json" = "^2.0"
|
|
68
|
+
"@std/http" = "1.0"
|
|
69
|
+
"package_a" = "latest"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Claims System
|
|
73
|
+
|
|
74
|
+
Packages declare platform support; CPM filters based on your target.
|
|
75
|
+
|
|
76
|
+
**Package metadata (registry):**
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"name": "win32-api",
|
|
80
|
+
"claims": { "os": ["windows"], "arch": ["x86_64"] }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Resolution:**
|
|
85
|
+
| Target | `win32-api` (windows) | `posix-api` (linux) | `@std/json` (any) |
|
|
86
|
+
|--------|----------------------|--------------------|--------------------|
|
|
87
|
+
| `linux/x86_64` | skip | install | install |
|
|
88
|
+
| `windows/x86_64` | install | skip | install |
|
|
89
|
+
|
|
90
|
+
Auto-detects from current platform when no target is specified.
|
|
91
|
+
|
|
92
|
+
## Prebuilt Mode
|
|
93
|
+
|
|
94
|
+
When the registry serves precompiled LLVM IR:
|
|
95
|
+
|
|
96
|
+
```toml
|
|
97
|
+
[cpm]
|
|
98
|
+
prebuilt = true
|
|
99
|
+
llvm_version = "18.1.0"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
CPM fetches `.ll` artifacts from `metadata/prebuilt/...` and skips source packages
|
|
103
|
+
with incompatible LLVM versions (major version must match).
|
|
104
|
+
|
|
105
|
+
## Lockfile (`cpm.lock`)
|
|
106
|
+
|
|
107
|
+
Auto-generated. Pins exact versions for reproducible builds.
|
|
108
|
+
|
|
109
|
+
```toml
|
|
110
|
+
[[package]]
|
|
111
|
+
name = "@std/json"
|
|
112
|
+
version = "2.0.3"
|
|
113
|
+
resolved = "https://repo.example.com/group/std/json/2.0.3.tar.gz"
|
|
114
|
+
checksum = "sha256:abc123..."
|
|
115
|
+
dependencies = ["@std/encoding@1.2.0"]
|
|
116
|
+
llvm_version = "18.1.0"
|
|
117
|
+
cpyte_version = "0.5.0"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Directory Layout
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
~/.cpm/
|
|
124
|
+
├── cache/<name>/<version>/ # downloaded archives
|
|
125
|
+
└── modules/<name>/<version>/ # extracted packages
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CPM - A package manager for Cpyte."""
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Structured types for parsed CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import List, Optional, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class GlobalOptions:
|
|
11
|
+
verbose: bool = False
|
|
12
|
+
quiet: bool = False
|
|
13
|
+
yes: bool = False
|
|
14
|
+
offline: bool = False
|
|
15
|
+
no_cache: bool = False
|
|
16
|
+
config: Optional[str] = None
|
|
17
|
+
server: List[str] = field(default_factory=list)
|
|
18
|
+
target: Optional[str] = None
|
|
19
|
+
llvm_version: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class InitCommand:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AddCommand:
|
|
29
|
+
packages: List[str] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class RemoveCommand:
|
|
34
|
+
packages: List[str] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class InstallCommand:
|
|
39
|
+
packages: List[str] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class UpdateCommand:
|
|
44
|
+
packages: List[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class BuildCommand:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class RunCommand:
|
|
54
|
+
script: str = ""
|
|
55
|
+
args: List[str] = field(default_factory=list)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class VersionCommand:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class PublishCommand:
|
|
65
|
+
directory: str = ""
|
|
66
|
+
name: str = ""
|
|
67
|
+
version: str = ""
|
|
68
|
+
requires: List[str] = field(default_factory=list)
|
|
69
|
+
prebuilt: bool = False
|
|
70
|
+
llvm_version: str = ""
|
|
71
|
+
cpyte_version: str = ""
|
|
72
|
+
server: str = ""
|
|
73
|
+
token: str = ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class UnpublishCommand:
|
|
78
|
+
name: str = ""
|
|
79
|
+
version: str = ""
|
|
80
|
+
all: bool = False
|
|
81
|
+
block: bool = False
|
|
82
|
+
server: str = ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class SearchCommand:
|
|
87
|
+
query: str = ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
Command = Union[
|
|
91
|
+
InitCommand,
|
|
92
|
+
AddCommand,
|
|
93
|
+
RemoveCommand,
|
|
94
|
+
InstallCommand,
|
|
95
|
+
UpdateCommand,
|
|
96
|
+
BuildCommand,
|
|
97
|
+
RunCommand,
|
|
98
|
+
VersionCommand,
|
|
99
|
+
PublishCommand,
|
|
100
|
+
UnpublishCommand,
|
|
101
|
+
SearchCommand,
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class ParsedCLI:
|
|
107
|
+
global_options: GlobalOptions = field(default_factory=GlobalOptions)
|
|
108
|
+
command: Optional[Command] = None
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""CLI error types for clear, user-facing error messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CLIError(Exception):
|
|
7
|
+
"""Base error for CLI parsing issues."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnknownCommandError(CLIError):
|
|
11
|
+
"""Raised when an unrecognized subcommand is given."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, command: str, suggestions: list[str] | None = None) -> None:
|
|
14
|
+
self.command = command
|
|
15
|
+
self.suggestions = suggestions or []
|
|
16
|
+
msg = f"unknown command '{command}'"
|
|
17
|
+
if self.suggestions:
|
|
18
|
+
msg += f". Did you mean: {', '.join(self.suggestions)}?"
|
|
19
|
+
super().__init__(msg)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MissingArgumentError(CLIError):
|
|
23
|
+
"""Raised when a required argument is missing."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, argument: str, command: str) -> None:
|
|
26
|
+
self.argument = argument
|
|
27
|
+
self.command = command
|
|
28
|
+
super().__init__(
|
|
29
|
+
f"command '{command}' requires {argument}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InvalidPackageSpecError(CLIError):
|
|
34
|
+
"""Raised when a package specification is invalid."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, spec: str) -> None:
|
|
37
|
+
self.spec = spec
|
|
38
|
+
super().__init__(f"invalid package specification: '{spec}'")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InvalidOptionError(CLIError):
|
|
42
|
+
"""Raised when an invalid option is provided."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, option: str, message: str = "") -> None:
|
|
45
|
+
self.option = option
|
|
46
|
+
msg = f"invalid option '{option}'"
|
|
47
|
+
if message:
|
|
48
|
+
msg += f": {message}"
|
|
49
|
+
super().__init__(msg)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Execution engine for CPM instruction streams.
|
|
2
|
+
|
|
3
|
+
Pipeline stage: Execute
|
|
4
|
+
Consumes the flat instruction list produced by deduplicator() and
|
|
5
|
+
performs actual filesystem operations.
|
|
6
|
+
|
|
7
|
+
Directory layout:
|
|
8
|
+
~/.cpm/cache/<name>/<version>/ — downloaded artifacts (shared cache)
|
|
9
|
+
<project>/.cpm/modules/<name>/<version>/ — installed packages (project-local)
|
|
10
|
+
|
|
11
|
+
Modes:
|
|
12
|
+
prebuilt = false (default): install source (.cpy files)
|
|
13
|
+
prebuilt = true: install precompiled LLVM IR (.ll)
|
|
14
|
+
registry serves prebuilt artifacts
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import tarfile
|
|
21
|
+
import tempfile
|
|
22
|
+
import zipfile
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import requests as rq
|
|
26
|
+
|
|
27
|
+
from .gethins import fetch_repo
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
CPM_HOME = Path.home() / ".cpm"
|
|
31
|
+
CACHE_DIR = CPM_HOME / "cache"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ensure_cache_dir():
|
|
35
|
+
"""Create CPM cache directory if it doesn't exist."""
|
|
36
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _cache_path(name: str, version: str) -> Path:
|
|
40
|
+
"""Return the cache directory for a specific package version."""
|
|
41
|
+
return CACHE_DIR / name / version
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _module_path(project_root: Path, name: str, version: str) -> Path:
|
|
45
|
+
"""Return the project-local install directory for a specific package version."""
|
|
46
|
+
return project_root / ".cpm" / "modules" / name / version
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def calculate_checksum(file_path: str, algorithm: str = "sha256") -> str:
|
|
50
|
+
"""Calculate checksum of a file."""
|
|
51
|
+
hash_func = hashlib.new(algorithm)
|
|
52
|
+
with open(file_path, "rb") as fp:
|
|
53
|
+
for chunk in iter(lambda: fp.read(4096), b""):
|
|
54
|
+
hash_func.update(chunk)
|
|
55
|
+
return f"{algorithm}:{hash_func.hexdigest()}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _verify_checksum(file_path: str, expected: str) -> None:
|
|
59
|
+
"""Verify a file's checksum. Expected format: 'algorithm:hex'."""
|
|
60
|
+
if ":" not in expected:
|
|
61
|
+
raise ValueError(f"Checksum must be 'algorithm:hex', got: {expected}")
|
|
62
|
+
algorithm, expected_hex = expected.split(":", 1)
|
|
63
|
+
actual = calculate_checksum(file_path, algorithm)
|
|
64
|
+
actual_hex = actual.split(":", 1)[1]
|
|
65
|
+
if actual_hex != expected_hex:
|
|
66
|
+
os.remove(file_path)
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Checksum mismatch for {file_path}: "
|
|
69
|
+
f"expected {expected}, got {actual}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _download(url: str, dest: Path) -> None:
|
|
74
|
+
"""Download a file from url to dest."""
|
|
75
|
+
response = rq.get(url, stream=True, timeout=30)
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
with open(dest, "wb") as fp:
|
|
78
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
79
|
+
fp.write(chunk)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _extract(archive_path: Path, dest: Path) -> None:
|
|
83
|
+
"""Extract a tar.gz or zip archive into dest."""
|
|
84
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
name = archive_path.name
|
|
86
|
+
if name.endswith(".tar.gz") or name.endswith(".tgz"):
|
|
87
|
+
with tarfile.open(archive_path, "r:gz") as tar:
|
|
88
|
+
tar.extractall(dest)
|
|
89
|
+
elif name.endswith(".tar.bz2"):
|
|
90
|
+
with tarfile.open(archive_path, "r:bz2") as tar:
|
|
91
|
+
tar.extractall(dest)
|
|
92
|
+
elif name.endswith(".tar"):
|
|
93
|
+
with tarfile.open(archive_path, "r") as tar:
|
|
94
|
+
tar.extractall(dest)
|
|
95
|
+
elif name.endswith(".zip"):
|
|
96
|
+
with zipfile.ZipFile(archive_path, "r") as zf:
|
|
97
|
+
zf.extractall(dest)
|
|
98
|
+
else:
|
|
99
|
+
# Raw file (e.g. .ll, .bc, .cpy) — copy directly
|
|
100
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
shutil.copy2(archive_path, dest / name)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_cached(name: str, version: str, checksum: str) -> bool:
|
|
105
|
+
"""Check if a package is already in cache with matching checksum."""
|
|
106
|
+
cache = _cache_path(name, version)
|
|
107
|
+
if not cache.exists():
|
|
108
|
+
return False
|
|
109
|
+
for f in cache.iterdir():
|
|
110
|
+
if f.is_file():
|
|
111
|
+
try:
|
|
112
|
+
actual = calculate_checksum(str(f))
|
|
113
|
+
if actual == checksum:
|
|
114
|
+
return True
|
|
115
|
+
except Exception:
|
|
116
|
+
continue
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _install_from_cache(project_root: Path, name: str, version: str) -> None:
|
|
121
|
+
"""Copy a cached archive into the project modules directory and extract."""
|
|
122
|
+
cache = _cache_path(name, version)
|
|
123
|
+
target = _module_path(project_root, name, version)
|
|
124
|
+
|
|
125
|
+
if target.exists():
|
|
126
|
+
shutil.rmtree(target)
|
|
127
|
+
|
|
128
|
+
for f in cache.iterdir():
|
|
129
|
+
if f.is_file():
|
|
130
|
+
_extract(f, target)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def execute_get(inst: dict, project_root: Path, prebuilt: bool = False) -> None:
|
|
135
|
+
"""Execute a single GET instruction.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
inst:
|
|
140
|
+
Instruction dict with GET, url, version, checksum.
|
|
141
|
+
project_root:
|
|
142
|
+
Project root directory (where .cpm/modules/ lives).
|
|
143
|
+
prebuilt:
|
|
144
|
+
If True, fetch prebuilt .ll from registry.
|
|
145
|
+
"""
|
|
146
|
+
name = inst["GET"]
|
|
147
|
+
url = inst.get("url")
|
|
148
|
+
version = inst.get("version", "latest")
|
|
149
|
+
checksum = inst.get("checksum")
|
|
150
|
+
|
|
151
|
+
if not url:
|
|
152
|
+
raise ValueError(f"No URL provided for package '{name}'")
|
|
153
|
+
|
|
154
|
+
target = _module_path(project_root, name, version)
|
|
155
|
+
|
|
156
|
+
# Already installed — skip
|
|
157
|
+
if target.exists():
|
|
158
|
+
print(f" {name}@{version} already installed")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
cache = _cache_path(name, version)
|
|
162
|
+
|
|
163
|
+
# Check cache
|
|
164
|
+
if checksum and _is_cached(name, version, checksum):
|
|
165
|
+
print(f" {name}@{version} found in cache, installing...")
|
|
166
|
+
_install_from_cache(project_root, name, version)
|
|
167
|
+
print(f" {name}@{version} installed")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Download
|
|
171
|
+
print(f" downloading {name}@{version}...")
|
|
172
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
filename = url.rsplit("/", 1)[-1] or f"{name}-{version}.tar.gz"
|
|
174
|
+
dest = cache / filename
|
|
175
|
+
_download(url, dest)
|
|
176
|
+
|
|
177
|
+
# Verify checksum
|
|
178
|
+
if checksum:
|
|
179
|
+
print(f" verifying checksum...")
|
|
180
|
+
_verify_checksum(str(dest), checksum)
|
|
181
|
+
|
|
182
|
+
# Extract to cache first
|
|
183
|
+
print(f" installing {name}@{version}...")
|
|
184
|
+
_extract(dest, cache)
|
|
185
|
+
|
|
186
|
+
# Install from cache to project
|
|
187
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
shutil.copytree(cache, target)
|
|
189
|
+
|
|
190
|
+
mode = "prebuilt" if prebuilt else "source"
|
|
191
|
+
print(f" {name}@{version} installed [{mode}]")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def execute_remove(inst: dict, project_root: Path) -> None:
|
|
195
|
+
"""Execute a single REMOVE instruction."""
|
|
196
|
+
name = inst["REMOVE"]
|
|
197
|
+
target = project_root / ".cpm" / "modules" / name
|
|
198
|
+
|
|
199
|
+
if not target.exists():
|
|
200
|
+
print(f" {name} not installed, skipping")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
shutil.rmtree(target)
|
|
204
|
+
print(f" {name} removed")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def execute(
|
|
208
|
+
instructions: list[dict],
|
|
209
|
+
project_root: Path,
|
|
210
|
+
repo: str = None,
|
|
211
|
+
ver: str = "latest",
|
|
212
|
+
prebuilt: bool = False,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Execute a flat instruction stream.
|
|
215
|
+
|
|
216
|
+
Pipeline stage: Execute
|
|
217
|
+
This is the final stage — instructions are consumed and filesystem
|
|
218
|
+
operations are performed.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
instructions:
|
|
223
|
+
Flat list of dicts from deduplicator(), e.g.
|
|
224
|
+
[{"GET": "D", "url": "...", ...}, {"GET": "B", ...}]
|
|
225
|
+
project_root:
|
|
226
|
+
Project root directory (where .cpm/modules/ lives).
|
|
227
|
+
repo:
|
|
228
|
+
Repository URL. If provided, missing metadata (url, checksum)
|
|
229
|
+
will be fetched during execution.
|
|
230
|
+
ver:
|
|
231
|
+
Package version to resolve against.
|
|
232
|
+
prebuilt:
|
|
233
|
+
If True, registry serves prebuilt artifacts (.ll).
|
|
234
|
+
"""
|
|
235
|
+
_ensure_cache_dir()
|
|
236
|
+
|
|
237
|
+
total = len(instructions)
|
|
238
|
+
mode = "prebuilt" if prebuilt else "source"
|
|
239
|
+
print(f"\nExecuting {total} instruction(s) [{mode}]...\n")
|
|
240
|
+
|
|
241
|
+
for i, inst in enumerate(instructions, 1):
|
|
242
|
+
# If metadata is missing from instruction, fetch it
|
|
243
|
+
if "GET" in inst and "url" not in inst:
|
|
244
|
+
name = inst["GET"]
|
|
245
|
+
if repo is None:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"Instruction for '{name}' has no URL and no repo provided"
|
|
248
|
+
)
|
|
249
|
+
metadata = fetch_repo(repo, f"metadata/{name}/{ver}")
|
|
250
|
+
inst["url"] = metadata["url"]
|
|
251
|
+
inst["checksum"] = metadata.get("checksum", "")
|
|
252
|
+
inst["version"] = metadata.get("version", ver)
|
|
253
|
+
|
|
254
|
+
if "GET" in inst:
|
|
255
|
+
print(f"[{i}/{total}] GET {inst['GET']}")
|
|
256
|
+
execute_get(inst, project_root, prebuilt=prebuilt)
|
|
257
|
+
elif "REMOVE" in inst:
|
|
258
|
+
print(f"[{i}/{total}] REMOVE {inst['REMOVE']}")
|
|
259
|
+
execute_remove(inst, project_root)
|
|
260
|
+
else:
|
|
261
|
+
print(f"[{i}/{total}] SKIP unknown instruction: {inst}")
|
|
262
|
+
|
|
263
|
+
print(f"\nDone. {total} instruction(s) executed.")
|