fspack 0.1.0__py3-none-any.whl
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.
- fspack/__init__.py +7 -0
- fspack/analyzer.py +296 -0
- fspack/builder.py +156 -0
- fspack/cli.py +87 -0
- fspack/commands/__init__.py +1 -0
- fspack/commands/build.py +25 -0
- fspack/commands/clean.py +21 -0
- fspack/commands/package.py +26 -0
- fspack/commands/run.py +36 -0
- fspack/config.py +92 -0
- fspack/embed.py +102 -0
- fspack/exceptions.py +36 -0
- fspack/installer.py +128 -0
- fspack/loader.py +207 -0
- fspack/mirror.py +35 -0
- fspack/platform.py +36 -0
- fspack/project.py +125 -0
- fspack/py.typed +0 -0
- fspack/standalone.py +82 -0
- fspack-0.1.0.dist-info/METADATA +136 -0
- fspack-0.1.0.dist-info/RECORD +24 -0
- fspack-0.1.0.dist-info/WHEEL +4 -0
- fspack-0.1.0.dist-info/entry_points.txt +2 -0
- fspack-0.1.0.dist-info/licenses/LICENSE +21 -0
fspack/__init__.py
ADDED
fspack/analyzer.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""AST 依赖分析:扫描 import,分类标准库/本地/第三方。."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fspack.config import DependencyReport
|
|
10
|
+
|
|
11
|
+
__all__ = ["STDLIB_FALLBACK", "analyze_dependencies", "collect_imports"]
|
|
12
|
+
|
|
13
|
+
# Python 3.8/3.9 没有 sys.stdlib_module_names,用 curate 的集合回退
|
|
14
|
+
STDLIB_FALLBACK: frozenset[str] = frozenset(
|
|
15
|
+
{
|
|
16
|
+
"abc",
|
|
17
|
+
"aifc",
|
|
18
|
+
"argparse",
|
|
19
|
+
"array",
|
|
20
|
+
"ast",
|
|
21
|
+
"asynchat",
|
|
22
|
+
"asyncio",
|
|
23
|
+
"asyncore",
|
|
24
|
+
"atexit",
|
|
25
|
+
"audioop",
|
|
26
|
+
"base64",
|
|
27
|
+
"bdb",
|
|
28
|
+
"binascii",
|
|
29
|
+
"binhex",
|
|
30
|
+
"bisect",
|
|
31
|
+
"builtins",
|
|
32
|
+
"bz2",
|
|
33
|
+
"calendar",
|
|
34
|
+
"cgi",
|
|
35
|
+
"cgitb",
|
|
36
|
+
"chunk",
|
|
37
|
+
"cmath",
|
|
38
|
+
"cmd",
|
|
39
|
+
"code",
|
|
40
|
+
"codecs",
|
|
41
|
+
"codeop",
|
|
42
|
+
"collections",
|
|
43
|
+
"colorsys",
|
|
44
|
+
"compileall",
|
|
45
|
+
"concurrent",
|
|
46
|
+
"configparser",
|
|
47
|
+
"contextlib",
|
|
48
|
+
"contextvars",
|
|
49
|
+
"copy",
|
|
50
|
+
"copyreg",
|
|
51
|
+
"crypt",
|
|
52
|
+
"csv",
|
|
53
|
+
"ctypes",
|
|
54
|
+
"curses",
|
|
55
|
+
"dataclasses",
|
|
56
|
+
"datetime",
|
|
57
|
+
"decimal",
|
|
58
|
+
"difflib",
|
|
59
|
+
"dis",
|
|
60
|
+
"distutils",
|
|
61
|
+
"doctest",
|
|
62
|
+
"email",
|
|
63
|
+
"encodings",
|
|
64
|
+
"enum",
|
|
65
|
+
"errno",
|
|
66
|
+
"faulthandler",
|
|
67
|
+
"fcntl",
|
|
68
|
+
"filecmp",
|
|
69
|
+
"fileinput",
|
|
70
|
+
"fnmatch",
|
|
71
|
+
"formatter",
|
|
72
|
+
"fractions",
|
|
73
|
+
"ftplib",
|
|
74
|
+
"functools",
|
|
75
|
+
"gc",
|
|
76
|
+
"genericpath",
|
|
77
|
+
"getopt",
|
|
78
|
+
"getpass",
|
|
79
|
+
"gettext",
|
|
80
|
+
"glob",
|
|
81
|
+
"graphlib",
|
|
82
|
+
"grp",
|
|
83
|
+
"gzip",
|
|
84
|
+
"hashlib",
|
|
85
|
+
"heapq",
|
|
86
|
+
"hmac",
|
|
87
|
+
"html",
|
|
88
|
+
"http",
|
|
89
|
+
"idlelib",
|
|
90
|
+
"imaplib",
|
|
91
|
+
"imghdr",
|
|
92
|
+
"imp",
|
|
93
|
+
"importlib",
|
|
94
|
+
"inspect",
|
|
95
|
+
"io",
|
|
96
|
+
"ipaddress",
|
|
97
|
+
"itertools",
|
|
98
|
+
"json",
|
|
99
|
+
"keyword",
|
|
100
|
+
"lib2to3",
|
|
101
|
+
"linecache",
|
|
102
|
+
"locale",
|
|
103
|
+
"logging",
|
|
104
|
+
"lzma",
|
|
105
|
+
"mailbox",
|
|
106
|
+
"mailcap",
|
|
107
|
+
"marshal",
|
|
108
|
+
"math",
|
|
109
|
+
"mimetypes",
|
|
110
|
+
"mmap",
|
|
111
|
+
"modulefinder",
|
|
112
|
+
"msilib",
|
|
113
|
+
"msvcrt",
|
|
114
|
+
"multiprocessing",
|
|
115
|
+
"netrc",
|
|
116
|
+
"nis",
|
|
117
|
+
"nntplib",
|
|
118
|
+
"ntpath",
|
|
119
|
+
"numbers",
|
|
120
|
+
"opcode",
|
|
121
|
+
"operator",
|
|
122
|
+
"optparse",
|
|
123
|
+
"os",
|
|
124
|
+
"ossaudiodev",
|
|
125
|
+
"parser",
|
|
126
|
+
"pathlib",
|
|
127
|
+
"pdb",
|
|
128
|
+
"pickle",
|
|
129
|
+
"pickletools",
|
|
130
|
+
"pipes",
|
|
131
|
+
"pkgutil",
|
|
132
|
+
"platform",
|
|
133
|
+
"plistlib",
|
|
134
|
+
"poplib",
|
|
135
|
+
"posix",
|
|
136
|
+
"posixpath",
|
|
137
|
+
"pprint",
|
|
138
|
+
"profile",
|
|
139
|
+
"pstats",
|
|
140
|
+
"pty",
|
|
141
|
+
"pwd",
|
|
142
|
+
"py_compile",
|
|
143
|
+
"pyclbr",
|
|
144
|
+
"pydoc",
|
|
145
|
+
"pydoc_data",
|
|
146
|
+
"pyexpat",
|
|
147
|
+
"queue",
|
|
148
|
+
"quopri",
|
|
149
|
+
"random",
|
|
150
|
+
"re",
|
|
151
|
+
"readline",
|
|
152
|
+
"reprlib",
|
|
153
|
+
"resource",
|
|
154
|
+
"rlcompleter",
|
|
155
|
+
"runpy",
|
|
156
|
+
"sched",
|
|
157
|
+
"secrets",
|
|
158
|
+
"select",
|
|
159
|
+
"selectors",
|
|
160
|
+
"shelve",
|
|
161
|
+
"shlex",
|
|
162
|
+
"shutil",
|
|
163
|
+
"signal",
|
|
164
|
+
"site",
|
|
165
|
+
"smtpd",
|
|
166
|
+
"smtplib",
|
|
167
|
+
"sndhdr",
|
|
168
|
+
"socket",
|
|
169
|
+
"socketserver",
|
|
170
|
+
"spwd",
|
|
171
|
+
"sqlite3",
|
|
172
|
+
"sre_compile",
|
|
173
|
+
"sre_constants",
|
|
174
|
+
"sre_parse",
|
|
175
|
+
"ssl",
|
|
176
|
+
"stat",
|
|
177
|
+
"statistics",
|
|
178
|
+
"string",
|
|
179
|
+
"stringprep",
|
|
180
|
+
"struct",
|
|
181
|
+
"subprocess",
|
|
182
|
+
"sunau",
|
|
183
|
+
"symbol",
|
|
184
|
+
"symtable",
|
|
185
|
+
"sys",
|
|
186
|
+
"sysconfig",
|
|
187
|
+
"syslog",
|
|
188
|
+
"tabnanny",
|
|
189
|
+
"tarfile",
|
|
190
|
+
"telnetlib",
|
|
191
|
+
"tempfile",
|
|
192
|
+
"termios",
|
|
193
|
+
"test",
|
|
194
|
+
"textwrap",
|
|
195
|
+
"threading",
|
|
196
|
+
"time",
|
|
197
|
+
"timeit",
|
|
198
|
+
"tkinter",
|
|
199
|
+
"token",
|
|
200
|
+
"tokenize",
|
|
201
|
+
"trace",
|
|
202
|
+
"traceback",
|
|
203
|
+
"tracemalloc",
|
|
204
|
+
"tty",
|
|
205
|
+
"turtle",
|
|
206
|
+
"types",
|
|
207
|
+
"typing",
|
|
208
|
+
"unicodedata",
|
|
209
|
+
"unittest",
|
|
210
|
+
"urllib",
|
|
211
|
+
"uu",
|
|
212
|
+
"uuid",
|
|
213
|
+
"venv",
|
|
214
|
+
"warnings",
|
|
215
|
+
"wave",
|
|
216
|
+
"weakref",
|
|
217
|
+
"webbrowser",
|
|
218
|
+
"winreg",
|
|
219
|
+
"winsound",
|
|
220
|
+
"wsgiref",
|
|
221
|
+
"xdrlib",
|
|
222
|
+
"xml",
|
|
223
|
+
"xmlrpc",
|
|
224
|
+
"zipapp",
|
|
225
|
+
"zipfile",
|
|
226
|
+
"zipimport",
|
|
227
|
+
"zlib",
|
|
228
|
+
"_thread",
|
|
229
|
+
"__future__",
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
_STDLIB: frozenset[str] = getattr(sys, "stdlib_module_names", STDLIB_FALLBACK)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def collect_imports(tree: ast.AST) -> list[str]:
|
|
237
|
+
"""收集 AST 中所有 import 的顶层模块名,去重保序。."""
|
|
238
|
+
result: list[str] = []
|
|
239
|
+
seen: set[str] = set()
|
|
240
|
+
for node in ast.walk(tree):
|
|
241
|
+
if isinstance(node, ast.Import):
|
|
242
|
+
for alias in node.names:
|
|
243
|
+
_push(alias.name.split(".")[0], result, seen)
|
|
244
|
+
elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
|
|
245
|
+
_push(node.module.split(".")[0], result, seen)
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _push(top: str, result: list[str], seen: set[str]) -> None:
|
|
250
|
+
if top and top not in seen:
|
|
251
|
+
seen.add(top)
|
|
252
|
+
result.append(top)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _local_packages(src_dir: Path, project_name: str) -> set[str]:
|
|
256
|
+
"""识别项目本地包/模块名(顶层 .py 与含 __init__.py 的目录)。."""
|
|
257
|
+
local: set[str] = {project_name}
|
|
258
|
+
for entry in src_dir.iterdir():
|
|
259
|
+
if entry.is_file() and entry.suffix == ".py":
|
|
260
|
+
local.add(entry.stem)
|
|
261
|
+
elif entry.is_dir() and (entry / "__init__.py").is_file():
|
|
262
|
+
local.add(entry.name)
|
|
263
|
+
return local
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def analyze_dependencies(src_dir: Path, project_name: str, declared: tuple[str, ...]) -> DependencyReport:
|
|
267
|
+
"""扫描 src_dir 下所有 .py,分类 import 为标准库/本地/第三方。."""
|
|
268
|
+
all_imports: list[str] = []
|
|
269
|
+
for py in src_dir.rglob("*.py"):
|
|
270
|
+
try:
|
|
271
|
+
tree = ast.parse(py.read_text(encoding="utf-8"))
|
|
272
|
+
except (SyntaxError, OSError):
|
|
273
|
+
continue
|
|
274
|
+
all_imports.extend(collect_imports(tree))
|
|
275
|
+
|
|
276
|
+
local = _local_packages(src_dir, project_name)
|
|
277
|
+
stdlib: list[str] = []
|
|
278
|
+
third: list[str] = []
|
|
279
|
+
local_imports: list[str] = []
|
|
280
|
+
seen: set[str] = set()
|
|
281
|
+
for imp in all_imports:
|
|
282
|
+
if imp in seen:
|
|
283
|
+
continue
|
|
284
|
+
seen.add(imp)
|
|
285
|
+
if imp in local:
|
|
286
|
+
local_imports.append(imp)
|
|
287
|
+
elif imp in _STDLIB:
|
|
288
|
+
stdlib.append(imp)
|
|
289
|
+
else:
|
|
290
|
+
third.append(imp)
|
|
291
|
+
return DependencyReport(
|
|
292
|
+
declared=declared,
|
|
293
|
+
ast_third_party=tuple(third),
|
|
294
|
+
ast_stdlib=tuple(stdlib),
|
|
295
|
+
ast_local=tuple(local_imports),
|
|
296
|
+
)
|
fspack/builder.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""构建流水线编排:解析 → embed → 依赖 → 源码 → loader。."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import zipfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from fspack.analyzer import analyze_dependencies
|
|
13
|
+
from fspack.config import BuildConfig, MirrorConfig, ProjectInfo
|
|
14
|
+
from fspack.embed import ensure_embed, write_pth
|
|
15
|
+
from fspack.exceptions import DependencyError
|
|
16
|
+
from fspack.loader import compile_loader, generate_loader_source
|
|
17
|
+
from fspack.platform import Platform, detect_platform, wheel_platform_tag
|
|
18
|
+
from fspack.project import DEFAULT_PY_VERSION, parse_project
|
|
19
|
+
from fspack.standalone import STANDALONE_RELEASE_TAG, ensure_standalone
|
|
20
|
+
|
|
21
|
+
__all__ = ["DEFAULT_PY_VERSION", "build", "copy_source", "download_wheels", "unpack_wheels"]
|
|
22
|
+
|
|
23
|
+
_logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_EXCLUDE = shutil.ignore_patterns(
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
".git",
|
|
29
|
+
"__pycache__",
|
|
30
|
+
"*.egg-info",
|
|
31
|
+
".venv",
|
|
32
|
+
".tox",
|
|
33
|
+
".fspack",
|
|
34
|
+
".trae",
|
|
35
|
+
".pytest_cache",
|
|
36
|
+
".ruff_cache",
|
|
37
|
+
".mypy_cache",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build( # noqa: PLR0913
|
|
42
|
+
project_dir: Path,
|
|
43
|
+
mirror: MirrorConfig,
|
|
44
|
+
py_version: str = DEFAULT_PY_VERSION,
|
|
45
|
+
dist_dir: Path | None = None,
|
|
46
|
+
embed_cache: Path | None = None,
|
|
47
|
+
target: Platform | None = None,
|
|
48
|
+
) -> ProjectInfo:
|
|
49
|
+
"""执行完整构建流水线,返回项目信息。."""
|
|
50
|
+
project_dir = Path(project_dir).resolve()
|
|
51
|
+
target = target or detect_platform()
|
|
52
|
+
dist = dist_dir or project_dir / "dist"
|
|
53
|
+
cache = embed_cache or Path.home() / ".fspack" / "cache" / "embed"
|
|
54
|
+
cfg = BuildConfig(project_dir=project_dir, dist_dir=dist, embed_cache_dir=cache, mirror=mirror, target=target)
|
|
55
|
+
info = parse_project(project_dir, py_version)
|
|
56
|
+
_logger.info("项目: %s %s (%s) 目标: %s", info.name, info.version, info.app_type.value, target.value)
|
|
57
|
+
|
|
58
|
+
runtime_dir = cfg.dist_dir / "runtime"
|
|
59
|
+
if target is Platform.LINUX:
|
|
60
|
+
standalone_cache = Path.home() / ".fspack" / "cache" / "standalone"
|
|
61
|
+
ensure_standalone(info.py_version, STANDALONE_RELEASE_TAG, standalone_cache, runtime_dir)
|
|
62
|
+
major, minor = info.py_version.split(".")[:2]
|
|
63
|
+
site_packages = runtime_dir / "python" / "lib" / f"python{major}.{minor}" / "site-packages"
|
|
64
|
+
else:
|
|
65
|
+
ensure_embed(info.py_version, cfg.mirror, cfg.embed_cache_dir, runtime_dir)
|
|
66
|
+
site_packages = runtime_dir / "Lib" / "site-packages"
|
|
67
|
+
|
|
68
|
+
report = analyze_dependencies(project_dir, info.name, info.dependencies)
|
|
69
|
+
if report.missing:
|
|
70
|
+
_logger.info("AST 发现未声明依赖: %s", ", ".join(report.missing))
|
|
71
|
+
if report.ast_third_party:
|
|
72
|
+
wheelhouse = cfg.dist_dir / "wheelhouse"
|
|
73
|
+
download_wheels(
|
|
74
|
+
report.ast_third_party,
|
|
75
|
+
info.py_version,
|
|
76
|
+
cfg.mirror.pypi_index,
|
|
77
|
+
wheelhouse,
|
|
78
|
+
platform_tag=wheel_platform_tag(target),
|
|
79
|
+
)
|
|
80
|
+
unpack_wheels(wheelhouse, site_packages)
|
|
81
|
+
else:
|
|
82
|
+
_logger.info("无第三方依赖,跳过 wheel 下载")
|
|
83
|
+
|
|
84
|
+
if target is Platform.WINDOWS:
|
|
85
|
+
write_pth(cfg.dist_dir, info.py_version)
|
|
86
|
+
src_dst = cfg.dist_dir / "src"
|
|
87
|
+
copy_source(project_dir, src_dst)
|
|
88
|
+
|
|
89
|
+
entry_rel = info.entry_file.relative_to(info.src_dir).as_posix()
|
|
90
|
+
source = generate_loader_source(f"src/{entry_rel}", info.py_xy, target)
|
|
91
|
+
exe_name = info.exe_name if target is Platform.WINDOWS else info.name
|
|
92
|
+
exe = cfg.dist_dir / exe_name
|
|
93
|
+
compile_loader(source, exe, info.app_type, cfg.dist_dir / "build", target)
|
|
94
|
+
_logger.info("构建完成: %s", exe)
|
|
95
|
+
return info
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def copy_source(project_dir: Path, src_dst: Path) -> None:
|
|
99
|
+
"""将项目源码复制到 dist/src,排除构建产物与缓存。."""
|
|
100
|
+
if src_dst.exists():
|
|
101
|
+
shutil.rmtree(src_dst)
|
|
102
|
+
shutil.copytree(project_dir, src_dst, ignore=_EXCLUDE)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def download_wheels(
|
|
106
|
+
packages: tuple[str, ...] | list[str],
|
|
107
|
+
py_version: str,
|
|
108
|
+
pypi_index: str,
|
|
109
|
+
wheelhouse_dir: Path,
|
|
110
|
+
platform_tag: str = "win_amd64",
|
|
111
|
+
) -> list[Path]:
|
|
112
|
+
"""用 dev python 的 pip 下载指定平台 wheel 到 wheelhouse 目录。."""
|
|
113
|
+
wheelhouse_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
major, minor = py_version.split(".")[:2]
|
|
115
|
+
cmd: list[str] = [
|
|
116
|
+
sys.executable,
|
|
117
|
+
"-m",
|
|
118
|
+
"pip",
|
|
119
|
+
"download",
|
|
120
|
+
"-d",
|
|
121
|
+
str(wheelhouse_dir),
|
|
122
|
+
"--platform",
|
|
123
|
+
platform_tag,
|
|
124
|
+
"--python-version",
|
|
125
|
+
f"{major}.{minor}",
|
|
126
|
+
"--abi",
|
|
127
|
+
f"cp{major}{minor}",
|
|
128
|
+
"--implementation",
|
|
129
|
+
"cp",
|
|
130
|
+
"--only-binary=:all:",
|
|
131
|
+
"-i",
|
|
132
|
+
pypi_index,
|
|
133
|
+
*packages,
|
|
134
|
+
]
|
|
135
|
+
_logger.info("下载依赖 wheel: %s", " ".join(packages))
|
|
136
|
+
try:
|
|
137
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
138
|
+
except FileNotFoundError as e:
|
|
139
|
+
raise DependencyError(f"未找到 pip: {sys.executable}") from e
|
|
140
|
+
except subprocess.CalledProcessError as e:
|
|
141
|
+
raise DependencyError(f"依赖下载失败:\n{e.stderr}") from e
|
|
142
|
+
return sorted(wheelhouse_dir.glob("*.whl"))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def unpack_wheels(wheelhouse_dir: Path, site_packages_dir: Path) -> int:
|
|
146
|
+
"""将 wheelhouse 内所有 .whl 解包到 site-packages 目录,返回解包数量。."""
|
|
147
|
+
site_packages_dir.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
count = 0
|
|
149
|
+
for whl in wheelhouse_dir.glob("*.whl"):
|
|
150
|
+
try:
|
|
151
|
+
with zipfile.ZipFile(whl) as zf:
|
|
152
|
+
zf.extractall(site_packages_dir)
|
|
153
|
+
except zipfile.BadZipFile as e:
|
|
154
|
+
raise DependencyError(f"wheel 损坏: {whl}") from e
|
|
155
|
+
count += 1
|
|
156
|
+
return count
|
fspack/cli.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""fspack CLI 入口 —— cargo 风格短命令(fsp b/c/r)。."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fspack import __version__
|
|
9
|
+
from fspack.commands import build as build_cmd
|
|
10
|
+
from fspack.commands import clean as clean_cmd
|
|
11
|
+
from fspack.commands import package as package_cmd
|
|
12
|
+
from fspack.commands import run as run_cmd
|
|
13
|
+
from fspack.mirror import MIRRORS
|
|
14
|
+
from fspack.platform import Platform
|
|
15
|
+
|
|
16
|
+
__all__ = ["build_parser", "main"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
"""构建参数解析器。."""
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="fspack",
|
|
23
|
+
description="极速 Python 打包器(cargo 风格短命令)。",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
26
|
+
|
|
27
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
28
|
+
|
|
29
|
+
p_build = sub.add_parser("build", aliases=["b"], help="打包项目")
|
|
30
|
+
p_build.add_argument("project", nargs="?", default=".", help="项目目录(默认当前目录)")
|
|
31
|
+
p_build.add_argument("--mirror", default=None, choices=list(MIRRORS), help="镜像源")
|
|
32
|
+
p_build.add_argument("--py-version", default=None, help="embed python 版本,如 3.11.9")
|
|
33
|
+
p_build.add_argument("--target", default=None, choices=["windows", "linux"], help="目标平台(默认当前平台)")
|
|
34
|
+
|
|
35
|
+
p_run = sub.add_parser("run", aliases=["r"], help="运行已打包项目")
|
|
36
|
+
p_run.add_argument("project", nargs="?", default=".", help="项目目录")
|
|
37
|
+
p_run.add_argument("rest", nargs=argparse.REMAINDER, default=[], help="透传给目标程序的参数(以 -- 分隔)")
|
|
38
|
+
|
|
39
|
+
p_clean = sub.add_parser("clean", aliases=["c"], help="清理 dist/")
|
|
40
|
+
p_clean.add_argument("project", nargs="?", default=".", help="项目目录")
|
|
41
|
+
|
|
42
|
+
p_pkg = sub.add_parser("package", aliases=["p"], help="生成 NSIS 安装包")
|
|
43
|
+
p_pkg.add_argument("project", nargs="?", default=".", help="项目目录")
|
|
44
|
+
p_pkg.add_argument("--mirror", default=None, choices=list(MIRRORS), help="镜像源")
|
|
45
|
+
p_pkg.add_argument("--py-version", default=None, help="embed python 版本,如 3.11.9")
|
|
46
|
+
p_pkg.add_argument("--no-build", action="store_true", help="跳过重建,直接打包已有 dist")
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv: list[str] | None = None) -> None:
|
|
51
|
+
"""主入口,解析参数并分发到子命令。."""
|
|
52
|
+
parser = build_parser()
|
|
53
|
+
ns = parser.parse_args(argv)
|
|
54
|
+
command = ns.command
|
|
55
|
+
if command is None:
|
|
56
|
+
parser.print_help()
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
project = Path(ns.project).resolve()
|
|
60
|
+
if command in ("build", "b"):
|
|
61
|
+
build_cmd.run(project, mirror=ns.mirror, py_version=ns.py_version, target=_parse_target(ns.target))
|
|
62
|
+
elif command in ("run", "r"):
|
|
63
|
+
run_cmd.run(project, rest_args=_drop_separator(ns.rest))
|
|
64
|
+
elif command in ("clean", "c"):
|
|
65
|
+
clean_cmd.run(project)
|
|
66
|
+
elif command in ("package", "p"):
|
|
67
|
+
package_cmd.run(project, mirror=ns.mirror, py_version=ns.py_version, no_build=ns.no_build)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _drop_separator(rest: list[str]) -> list[str]:
|
|
71
|
+
"""剔除 argparse REMAINDER 捕获的首个 -- 分隔符。."""
|
|
72
|
+
if rest and rest[0] == "--":
|
|
73
|
+
return rest[1:]
|
|
74
|
+
return rest
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_target(value: str | None) -> Platform | None:
|
|
78
|
+
"""将 CLI 字符串转为 Platform 枚举,None 表示用当前平台。."""
|
|
79
|
+
if value is None:
|
|
80
|
+
return None
|
|
81
|
+
if value == "windows":
|
|
82
|
+
return Platform.WINDOWS
|
|
83
|
+
return Platform.LINUX
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""fspack 子命令实现:build / clean / run。."""
|
fspack/commands/build.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""fsp b —— 打包项目。."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fspack.builder import DEFAULT_PY_VERSION, build
|
|
9
|
+
from fspack.mirror import get_mirror
|
|
10
|
+
from fspack.platform import Platform
|
|
11
|
+
|
|
12
|
+
__all__ = ["run"]
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(
|
|
18
|
+
project: Path,
|
|
19
|
+
mirror: str | None = None,
|
|
20
|
+
py_version: str | None = None,
|
|
21
|
+
target: Platform | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""执行项目构建。."""
|
|
24
|
+
mirror_cfg = get_mirror(mirror)
|
|
25
|
+
build(project, mirror_cfg, py_version or DEFAULT_PY_VERSION, target=target)
|
fspack/commands/clean.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""fsp c —— 清理 dist/。."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
__all__ = ["run"]
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(project: Path) -> None:
|
|
15
|
+
"""清理项目下的 dist 目录。."""
|
|
16
|
+
dist = Path(project) / "dist"
|
|
17
|
+
if dist.is_dir():
|
|
18
|
+
shutil.rmtree(dist)
|
|
19
|
+
_logger.info("已清理: %s", dist)
|
|
20
|
+
else:
|
|
21
|
+
_logger.info("无 dist 目录可清理: %s", dist)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""fsp p —— 生成 NSIS 安装包。."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fspack.installer import build_installer
|
|
9
|
+
from fspack.mirror import get_mirror
|
|
10
|
+
from fspack.project import DEFAULT_PY_VERSION
|
|
11
|
+
|
|
12
|
+
__all__ = ["run"]
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(
|
|
18
|
+
project: Path,
|
|
19
|
+
mirror: str | None = None,
|
|
20
|
+
py_version: str | None = None,
|
|
21
|
+
no_build: bool = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""生成 Windows 安装包到 dist/release/。."""
|
|
24
|
+
mirror_cfg = get_mirror(mirror)
|
|
25
|
+
out = build_installer(project, mirror_cfg, py_version or DEFAULT_PY_VERSION, no_build=no_build)
|
|
26
|
+
_logger.info("安装包已生成: %s", out)
|
fspack/commands/run.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""fsp r —— 运行已打包项目(Linux 用 wine,Windows 直跑)。."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from fspack.exceptions import FspackError
|
|
12
|
+
from fspack.project import parse_project
|
|
13
|
+
|
|
14
|
+
__all__ = ["run"]
|
|
15
|
+
|
|
16
|
+
_logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(project: Path, rest_args: list[str] | None = None) -> None:
|
|
20
|
+
"""运行 dist 下的可执行文件。."""
|
|
21
|
+
info = parse_project(project)
|
|
22
|
+
exe = Path(project) / "dist" / info.exe_name
|
|
23
|
+
if not exe.is_file():
|
|
24
|
+
raise FspackError(f"未找到已构建的可执行文件: {exe}(请先执行 fsp b)")
|
|
25
|
+
cmd = _build_cmd(exe) + (rest_args or [])
|
|
26
|
+
_logger.info("运行: %s", " ".join(cmd))
|
|
27
|
+
completed = subprocess.run(cmd, check=False)
|
|
28
|
+
if completed.returncode != 0:
|
|
29
|
+
raise FspackError(f"程序退出码非零: {completed.returncode}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_cmd(exe: Path) -> list[str]:
|
|
33
|
+
if platform.system() == "Linux":
|
|
34
|
+
wine = shutil.which("wine") or "wine"
|
|
35
|
+
return [wine, str(exe)]
|
|
36
|
+
return [str(exe)]
|