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 ADDED
@@ -0,0 +1,7 @@
1
+ """fspack - 极速 Python 项目打包器。."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["__version__"]
6
+
7
+ __version__ = "0.1.0"
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。."""
@@ -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)
@@ -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)]