applepy-cli 0.1.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.
- applepy_cli-0.1.0/PKG-INFO +11 -0
- applepy_cli-0.1.0/applepy_cli.egg-info/PKG-INFO +11 -0
- applepy_cli-0.1.0/applepy_cli.egg-info/SOURCES.txt +7 -0
- applepy_cli-0.1.0/applepy_cli.egg-info/dependency_links.txt +1 -0
- applepy_cli-0.1.0/applepy_cli.egg-info/entry_points.txt +2 -0
- applepy_cli-0.1.0/applepy_cli.egg-info/top_level.txt +1 -0
- applepy_cli-0.1.0/applepy_cli.py +647 -0
- applepy_cli-0.1.0/pyproject.toml +23 -0
- applepy_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: applepy-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build tool for ApplePy — scaffold, build, and publish Swift-powered Python packages
|
|
5
|
+
Author: Jagtesh Chadha
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Operating System :: MacOS
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
11
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: applepy-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build tool for ApplePy — scaffold, build, and publish Swift-powered Python packages
|
|
5
|
+
Author: Jagtesh Chadha
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Operating System :: MacOS
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
11
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
applepy_cli
|
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""applepy — Build tool for ApplePy projects.
|
|
3
|
+
|
|
4
|
+
Equivalent to maturin for PyO3. Scaffolds, builds, and publishes
|
|
5
|
+
Swift-powered Python extension modules.
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
applepy new <name> Create a new project
|
|
9
|
+
applepy develop Build and install into current env
|
|
10
|
+
applepy build Build a distributable wheel
|
|
11
|
+
applepy publish Publish to PyPI
|
|
12
|
+
"""
|
|
13
|
+
import argparse
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import sysconfig
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from textwrap import dedent
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
# ApplePy Swift package — used when generating Package.swift for new projects
|
|
27
|
+
APPLEPY_GITHUB_URL = "https://github.com/jagtesh/ApplePy.git"
|
|
28
|
+
APPLEPY_MIN_VERSION = "1.0.0"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Templates ───────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
PYPROJECT_TEMPLATE = '''\
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["setuptools>=68.0"]
|
|
36
|
+
build-backend = "setuptools.build_meta"
|
|
37
|
+
|
|
38
|
+
[project]
|
|
39
|
+
name = "{name}"
|
|
40
|
+
version = "0.1.0"
|
|
41
|
+
description = "{description}"
|
|
42
|
+
readme = "README.md"
|
|
43
|
+
license = {{text = "BSD-3-Clause"}}
|
|
44
|
+
requires-python = ">=3.10"
|
|
45
|
+
classifiers = [
|
|
46
|
+
"Development Status :: 3 - Alpha",
|
|
47
|
+
"Operating System :: MacOS",
|
|
48
|
+
"Programming Language :: Python :: 3",
|
|
49
|
+
"Programming Language :: Swift",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
include = ["{name}*"]
|
|
54
|
+
'''
|
|
55
|
+
|
|
56
|
+
SETUP_PY_TEMPLATE = '''\
|
|
57
|
+
"""Build — compiles Swift source into a Python-loadable .so"""
|
|
58
|
+
import os
|
|
59
|
+
import subprocess
|
|
60
|
+
import sys
|
|
61
|
+
import sysconfig
|
|
62
|
+
from pathlib import Path
|
|
63
|
+
|
|
64
|
+
from setuptools import setup
|
|
65
|
+
from setuptools.command.build_ext import build_ext
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SwiftBuildExt(build_ext):
|
|
69
|
+
"""Custom build_ext that calls `swift build` to compile the Swift extension."""
|
|
70
|
+
|
|
71
|
+
def run(self):
|
|
72
|
+
if sys.platform != "darwin":
|
|
73
|
+
raise RuntimeError("{name} only supports macOS")
|
|
74
|
+
|
|
75
|
+
swift_dir = Path(__file__).parent / "swift"
|
|
76
|
+
pkg_config_path = sysconfig.get_config_var("LIBPC") or ""
|
|
77
|
+
|
|
78
|
+
env = os.environ.copy()
|
|
79
|
+
env["PKG_CONFIG_PATH"] = pkg_config_path
|
|
80
|
+
|
|
81
|
+
print("🔨 Building Swift extension...")
|
|
82
|
+
subprocess.check_call(
|
|
83
|
+
["swift", "build"],
|
|
84
|
+
cwd=swift_dir,
|
|
85
|
+
env=env,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
build_dir = swift_dir / ".build" / "debug"
|
|
89
|
+
dylib = build_dir / "lib{swift_target}.dylib"
|
|
90
|
+
if not dylib.exists():
|
|
91
|
+
raise RuntimeError(f"Build succeeded but {{dylib}} not found")
|
|
92
|
+
|
|
93
|
+
dest = Path(__file__).parent / "{name}" / "{name}.so"
|
|
94
|
+
print(f"📦 Installing {{dylib.name}} → {{dest}}")
|
|
95
|
+
import shutil
|
|
96
|
+
shutil.copy2(dylib, dest)
|
|
97
|
+
|
|
98
|
+
def get_ext_filename(self, ext_name):
|
|
99
|
+
return ext_name + ".so"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
setup(cmdclass={{"build_ext": SwiftBuildExt}})
|
|
103
|
+
'''
|
|
104
|
+
|
|
105
|
+
INIT_PY_TEMPLATE = '''\
|
|
106
|
+
"""{name} — {description}
|
|
107
|
+
|
|
108
|
+
Powered by Swift & ApplePy.
|
|
109
|
+
"""
|
|
110
|
+
import importlib
|
|
111
|
+
import os
|
|
112
|
+
import sys
|
|
113
|
+
|
|
114
|
+
if sys.platform != "darwin":
|
|
115
|
+
raise ImportError("{name} only supports macOS")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _load_native():
|
|
119
|
+
"""Load the compiled Swift extension module."""
|
|
120
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
121
|
+
so_path = os.path.join(pkg_dir, "{name}.so")
|
|
122
|
+
|
|
123
|
+
if not os.path.exists(so_path):
|
|
124
|
+
raise ImportError(
|
|
125
|
+
"Native extension not found. Build it first:\\n"
|
|
126
|
+
" applepy develop\\n"
|
|
127
|
+
" # or: pip install -e ."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
spec = importlib.util.spec_from_file_location("{name}", so_path)
|
|
131
|
+
mod = importlib.util.module_from_spec(spec)
|
|
132
|
+
spec.loader.exec_module(mod)
|
|
133
|
+
return mod
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_native = _load_native()
|
|
137
|
+
|
|
138
|
+
# Re-export all public attributes from the native module
|
|
139
|
+
for _attr in dir(_native):
|
|
140
|
+
if not _attr.startswith("_"):
|
|
141
|
+
globals()[_attr] = getattr(_native, _attr)
|
|
142
|
+
|
|
143
|
+
__version__ = "0.1.0"
|
|
144
|
+
'''
|
|
145
|
+
|
|
146
|
+
# Two Package.swift templates: one for GitHub (default), one for local dev
|
|
147
|
+
PACKAGE_SWIFT_TEMPLATE_GITHUB = '''\
|
|
148
|
+
// swift-tools-version: 6.0
|
|
149
|
+
import PackageDescription
|
|
150
|
+
|
|
151
|
+
let package = Package(
|
|
152
|
+
name: "{swift_target}",
|
|
153
|
+
platforms: [.macOS(.v14)],
|
|
154
|
+
products: [
|
|
155
|
+
.library(name: "{swift_target}", type: .dynamic, targets: ["{swift_target}"]),
|
|
156
|
+
],
|
|
157
|
+
dependencies: [
|
|
158
|
+
.package(url: "{applepy_url}", from: "{applepy_version}"),
|
|
159
|
+
],
|
|
160
|
+
targets: [
|
|
161
|
+
.target(
|
|
162
|
+
name: "{swift_target}",
|
|
163
|
+
dependencies: [
|
|
164
|
+
.product(name: "ApplePy", package: "ApplePy"),
|
|
165
|
+
.product(name: "ApplePyClient", package: "ApplePy"),
|
|
166
|
+
]
|
|
167
|
+
),
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
'''
|
|
171
|
+
|
|
172
|
+
PACKAGE_SWIFT_TEMPLATE_LOCAL = '''\
|
|
173
|
+
// swift-tools-version: 6.0
|
|
174
|
+
import PackageDescription
|
|
175
|
+
|
|
176
|
+
let package = Package(
|
|
177
|
+
name: "{swift_target}",
|
|
178
|
+
platforms: [.macOS(.v14)],
|
|
179
|
+
products: [
|
|
180
|
+
.library(name: "{swift_target}", type: .dynamic, targets: ["{swift_target}"]),
|
|
181
|
+
],
|
|
182
|
+
dependencies: [
|
|
183
|
+
.package(path: "{applepy_path}"),
|
|
184
|
+
],
|
|
185
|
+
targets: [
|
|
186
|
+
.target(
|
|
187
|
+
name: "{swift_target}",
|
|
188
|
+
dependencies: [
|
|
189
|
+
.product(name: "ApplePy", package: "ApplePy"),
|
|
190
|
+
.product(name: "ApplePyClient", package: "ApplePy"),
|
|
191
|
+
]
|
|
192
|
+
),
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
'''
|
|
196
|
+
|
|
197
|
+
SWIFT_SOURCE_TEMPLATE = '''\
|
|
198
|
+
// {swift_target} — Powered by ApplePy
|
|
199
|
+
//
|
|
200
|
+
// Usage from Python:
|
|
201
|
+
// import {name}
|
|
202
|
+
// print({name}.hello("world"))
|
|
203
|
+
|
|
204
|
+
import ApplePy
|
|
205
|
+
@preconcurrency import ApplePyFFI
|
|
206
|
+
|
|
207
|
+
// MARK: - Functions
|
|
208
|
+
|
|
209
|
+
@PyFunction
|
|
210
|
+
func hello(name: String = "World") -> String {{
|
|
211
|
+
return "Hello, \\(name)! 🍎"
|
|
212
|
+
}}
|
|
213
|
+
|
|
214
|
+
// MARK: - Module Entry Point
|
|
215
|
+
|
|
216
|
+
@PyModule("{name}", functions: [
|
|
217
|
+
hello,
|
|
218
|
+
])
|
|
219
|
+
func {name}() {{}}
|
|
220
|
+
'''
|
|
221
|
+
|
|
222
|
+
DEMO_PY_TEMPLATE = '''\
|
|
223
|
+
#!/usr/bin/env python3
|
|
224
|
+
"""{swift_target} — Example Usage"""
|
|
225
|
+
import {name}
|
|
226
|
+
|
|
227
|
+
print({name}.hello("World"))
|
|
228
|
+
print({name}.hello("ApplePy"))
|
|
229
|
+
'''
|
|
230
|
+
|
|
231
|
+
README_TEMPLATE = '''\
|
|
232
|
+
# {swift_target}
|
|
233
|
+
|
|
234
|
+
{description}
|
|
235
|
+
|
|
236
|
+
> **macOS only** — requires Swift 6.0+ and ApplePy
|
|
237
|
+
|
|
238
|
+
## Install
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
applepy develop
|
|
242
|
+
# or: pip install -e .
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Usage
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
import {name}
|
|
249
|
+
|
|
250
|
+
print({name}.hello("World")) # Hello, World! 🍎
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Development
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
applepy develop # Build Swift + install
|
|
257
|
+
applepy build # Build wheel
|
|
258
|
+
applepy publish # Publish to PyPI
|
|
259
|
+
```
|
|
260
|
+
'''
|
|
261
|
+
|
|
262
|
+
GITIGNORE_TEMPLATE = '''\
|
|
263
|
+
*.so
|
|
264
|
+
swift/.build/
|
|
265
|
+
*.egg-info/
|
|
266
|
+
dist/
|
|
267
|
+
build/
|
|
268
|
+
__pycache__/
|
|
269
|
+
.DS_Store
|
|
270
|
+
'''
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ── Helpers ─────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
def _get_platform_tag():
|
|
276
|
+
"""Get the wheel platform tag for the current macOS + arch."""
|
|
277
|
+
# e.g., macosx_14_0_arm64
|
|
278
|
+
mac_ver = platform.mac_ver()[0] # "14.3.1"
|
|
279
|
+
parts = mac_ver.split(".")
|
|
280
|
+
major = parts[0] if parts else "14"
|
|
281
|
+
minor = parts[1] if len(parts) > 1 else "0"
|
|
282
|
+
arch = platform.machine() # "arm64" or "x86_64"
|
|
283
|
+
return f"macosx_{major}_{minor}_{arch}"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _find_applepy():
|
|
287
|
+
"""Try to find a local ApplePy package."""
|
|
288
|
+
cli_dir = Path(__file__).resolve().parent
|
|
289
|
+
candidates = [
|
|
290
|
+
cli_dir.parent.parent, # Tools/applepy-cli -> ApplePy
|
|
291
|
+
cli_dir.parent.parent.parent / "ApplePy", # sibling in workspace
|
|
292
|
+
Path.cwd().parent / "ApplePy",
|
|
293
|
+
]
|
|
294
|
+
for p in candidates:
|
|
295
|
+
if (p / "Package.swift").exists() and (p / "Sources").exists():
|
|
296
|
+
return p
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _load_project(project_dir: Path) -> dict:
|
|
301
|
+
"""Load project configuration from pyproject.toml."""
|
|
302
|
+
pyproject = project_dir / "pyproject.toml"
|
|
303
|
+
if not pyproject.exists():
|
|
304
|
+
print(f"❌ No pyproject.toml found. Are you in a project directory?")
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
|
|
307
|
+
name = None
|
|
308
|
+
content = pyproject.read_text()
|
|
309
|
+
for line in content.splitlines():
|
|
310
|
+
line = line.strip()
|
|
311
|
+
if line.startswith("name") and "=" in line:
|
|
312
|
+
val = line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
313
|
+
name = val
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
if not name:
|
|
317
|
+
print(f"❌ Could not find project name in pyproject.toml")
|
|
318
|
+
sys.exit(1)
|
|
319
|
+
|
|
320
|
+
# Derive swift target from name
|
|
321
|
+
swift_target = "".join(word.capitalize() for word in name.split("_")) or name.capitalize()
|
|
322
|
+
|
|
323
|
+
# Check if swift/Package.swift has a different target name
|
|
324
|
+
pkg_swift = project_dir / "swift" / "Package.swift"
|
|
325
|
+
if pkg_swift.exists():
|
|
326
|
+
pkg_content = pkg_swift.read_text()
|
|
327
|
+
match = re.search(r'Package\(\s*name:\s*"([^"]+)"', pkg_content)
|
|
328
|
+
if match:
|
|
329
|
+
swift_target = match.group(1)
|
|
330
|
+
|
|
331
|
+
return {"name": name, "swift_target": swift_target}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _swift_build(swift_dir: Path, swift_target: str):
|
|
335
|
+
"""Run swift build and return the path to the built dylib."""
|
|
336
|
+
pkg_config_path = sysconfig.get_config_var("LIBPC") or ""
|
|
337
|
+
env = os.environ.copy()
|
|
338
|
+
env["PKG_CONFIG_PATH"] = pkg_config_path
|
|
339
|
+
|
|
340
|
+
print(f"🔨 Building {swift_target}...")
|
|
341
|
+
try:
|
|
342
|
+
subprocess.check_call(["swift", "build"], cwd=swift_dir, env=env)
|
|
343
|
+
except subprocess.CalledProcessError:
|
|
344
|
+
print(f"❌ Swift build failed")
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
|
|
347
|
+
dylib = swift_dir / ".build" / "debug" / f"lib{swift_target}.dylib"
|
|
348
|
+
if not dylib.exists():
|
|
349
|
+
print(f"❌ Expected {dylib} not found")
|
|
350
|
+
sys.exit(1)
|
|
351
|
+
|
|
352
|
+
return dylib
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ── Commands ────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
def cmd_new(args):
|
|
358
|
+
"""Scaffold a new ApplePy project."""
|
|
359
|
+
name = args.name.lower().replace("-", "_").replace(" ", "_")
|
|
360
|
+
swift_target = "".join(word.capitalize() for word in name.split("_")) or name.capitalize()
|
|
361
|
+
project_dir = Path(args.name)
|
|
362
|
+
description = args.description or "A Swift-powered Python package"
|
|
363
|
+
|
|
364
|
+
if project_dir.exists():
|
|
365
|
+
print(f"❌ Directory '{args.name}' already exists")
|
|
366
|
+
sys.exit(1)
|
|
367
|
+
|
|
368
|
+
print(f"🍎 Creating new ApplePy project: {name}")
|
|
369
|
+
print(f" Swift target: {swift_target}")
|
|
370
|
+
|
|
371
|
+
# Determine ApplePy dependency mode
|
|
372
|
+
use_local = args.local
|
|
373
|
+
if use_local:
|
|
374
|
+
applepy_path = args.applepy_path or _find_applepy()
|
|
375
|
+
if not applepy_path:
|
|
376
|
+
print(f" ⚠ Could not auto-detect local ApplePy. Using default path.")
|
|
377
|
+
applepy_path = Path("../../ApplePy")
|
|
378
|
+
else:
|
|
379
|
+
applepy_path = Path(applepy_path).resolve()
|
|
380
|
+
# Compute relative path from swift/ to ApplePy
|
|
381
|
+
swift_dir = project_dir.resolve() / "swift"
|
|
382
|
+
try:
|
|
383
|
+
rel_applepy = os.path.relpath(applepy_path, swift_dir)
|
|
384
|
+
except ValueError:
|
|
385
|
+
rel_applepy = str(applepy_path)
|
|
386
|
+
print(f" ApplePy: local ({rel_applepy})")
|
|
387
|
+
else:
|
|
388
|
+
applepy_url = args.applepy_url or APPLEPY_GITHUB_URL
|
|
389
|
+
applepy_version = args.applepy_version or APPLEPY_MIN_VERSION
|
|
390
|
+
print(f" ApplePy: {applepy_url} (>= {applepy_version})")
|
|
391
|
+
print()
|
|
392
|
+
|
|
393
|
+
# Create directory structure
|
|
394
|
+
dirs = [
|
|
395
|
+
project_dir,
|
|
396
|
+
project_dir / name,
|
|
397
|
+
project_dir / name / "examples",
|
|
398
|
+
project_dir / "swift",
|
|
399
|
+
project_dir / "swift" / "Sources" / swift_target,
|
|
400
|
+
]
|
|
401
|
+
for d in dirs:
|
|
402
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
403
|
+
|
|
404
|
+
# Build context for templates
|
|
405
|
+
ctx = {
|
|
406
|
+
"name": name,
|
|
407
|
+
"swift_target": swift_target,
|
|
408
|
+
"description": description,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Choose Package.swift template
|
|
412
|
+
if use_local:
|
|
413
|
+
ctx["applepy_path"] = rel_applepy
|
|
414
|
+
pkg_swift_template = PACKAGE_SWIFT_TEMPLATE_LOCAL
|
|
415
|
+
else:
|
|
416
|
+
ctx["applepy_url"] = applepy_url
|
|
417
|
+
ctx["applepy_version"] = applepy_version
|
|
418
|
+
pkg_swift_template = PACKAGE_SWIFT_TEMPLATE_GITHUB
|
|
419
|
+
|
|
420
|
+
files = {
|
|
421
|
+
project_dir / "pyproject.toml": PYPROJECT_TEMPLATE,
|
|
422
|
+
project_dir / "setup.py": SETUP_PY_TEMPLATE,
|
|
423
|
+
project_dir / "README.md": README_TEMPLATE,
|
|
424
|
+
project_dir / ".gitignore": GITIGNORE_TEMPLATE,
|
|
425
|
+
project_dir / name / "__init__.py": INIT_PY_TEMPLATE,
|
|
426
|
+
project_dir / name / "examples" / "demo.py": DEMO_PY_TEMPLATE,
|
|
427
|
+
project_dir / "swift" / "Package.swift": pkg_swift_template,
|
|
428
|
+
project_dir / "swift" / "Sources" / swift_target / f"{swift_target}.swift": SWIFT_SOURCE_TEMPLATE,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for filepath, template in files.items():
|
|
432
|
+
content = template.format(**ctx)
|
|
433
|
+
filepath.write_text(content)
|
|
434
|
+
rel = filepath.relative_to(project_dir)
|
|
435
|
+
print(f" ✓ {rel}")
|
|
436
|
+
|
|
437
|
+
# Init git
|
|
438
|
+
if shutil.which("git"):
|
|
439
|
+
subprocess.run(["git", "init", "-q"], cwd=project_dir)
|
|
440
|
+
subprocess.run(["git", "add", "-A"], cwd=project_dir)
|
|
441
|
+
subprocess.run(
|
|
442
|
+
["git", "commit", "-q", "-m", f"Initial ApplePy project: {name}"],
|
|
443
|
+
cwd=project_dir,
|
|
444
|
+
)
|
|
445
|
+
print(f" ✓ git initialized")
|
|
446
|
+
|
|
447
|
+
print()
|
|
448
|
+
print(f"🎉 Project created! Next steps:")
|
|
449
|
+
print(f" cd {args.name}")
|
|
450
|
+
print(f" applepy develop")
|
|
451
|
+
print(f" python {name}/examples/demo.py")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def cmd_develop(args):
|
|
455
|
+
"""Build Swift extension and install into current environment."""
|
|
456
|
+
project_dir = Path.cwd()
|
|
457
|
+
config = _load_project(project_dir)
|
|
458
|
+
name = config["name"]
|
|
459
|
+
swift_target = config["swift_target"]
|
|
460
|
+
|
|
461
|
+
swift_dir = project_dir / "swift"
|
|
462
|
+
if not swift_dir.exists():
|
|
463
|
+
print(f"❌ No swift/ directory found. Are you in an ApplePy project?")
|
|
464
|
+
sys.exit(1)
|
|
465
|
+
|
|
466
|
+
dylib = _swift_build(swift_dir, swift_target)
|
|
467
|
+
|
|
468
|
+
dest = project_dir / name / f"{name}.so"
|
|
469
|
+
shutil.copy2(dylib, dest)
|
|
470
|
+
print(f"📦 Installed {dylib.name} → {dest.relative_to(project_dir)}")
|
|
471
|
+
|
|
472
|
+
print(f"📥 Installing {name} into current environment...")
|
|
473
|
+
subprocess.check_call(
|
|
474
|
+
[sys.executable, "-m", "pip", "install", "-e", ".", "-q"],
|
|
475
|
+
cwd=project_dir,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
print(f"✅ Done! Try: python -c \"import {name}; print(dir({name}))\"")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def cmd_build(args):
|
|
482
|
+
"""Build a distributable wheel with the native .so baked in."""
|
|
483
|
+
project_dir = Path.cwd()
|
|
484
|
+
config = _load_project(project_dir)
|
|
485
|
+
name = config["name"]
|
|
486
|
+
swift_target = config["swift_target"]
|
|
487
|
+
|
|
488
|
+
swift_dir = project_dir / "swift"
|
|
489
|
+
dylib = _swift_build(swift_dir, swift_target)
|
|
490
|
+
|
|
491
|
+
dest = project_dir / name / f"{name}.so"
|
|
492
|
+
shutil.copy2(dylib, dest)
|
|
493
|
+
print(f"📦 Installed {dylib.name} → {dest.relative_to(project_dir)}")
|
|
494
|
+
|
|
495
|
+
# Build wheel with correct platform tag
|
|
496
|
+
dist_dir = project_dir / "dist"
|
|
497
|
+
dist_dir.mkdir(exist_ok=True)
|
|
498
|
+
|
|
499
|
+
plat_tag = _get_platform_tag()
|
|
500
|
+
print(f"📦 Building wheel (platform: {plat_tag})...")
|
|
501
|
+
subprocess.check_call(
|
|
502
|
+
[
|
|
503
|
+
sys.executable, "-m", "pip", "wheel", ".",
|
|
504
|
+
"-w", "dist", "-q", "--no-deps",
|
|
505
|
+
],
|
|
506
|
+
cwd=project_dir,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Rename the wheel to use the correct platform tag
|
|
510
|
+
# setuptools generates py3-none-any, we need macosx_XX_X_arch
|
|
511
|
+
for whl in dist_dir.glob(f"{name}-*-py3-none-any.whl"):
|
|
512
|
+
correct_name = whl.name.replace("py3-none-any", f"py3-none-{plat_tag}")
|
|
513
|
+
correct_path = whl.parent / correct_name
|
|
514
|
+
whl.rename(correct_path)
|
|
515
|
+
size_kb = correct_path.stat().st_size / 1024
|
|
516
|
+
print(f"✅ Built: {correct_name} ({size_kb:.0f} KB)")
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
# If already has correct tag, just report
|
|
520
|
+
wheels = list(dist_dir.glob(f"{name}-*.whl"))
|
|
521
|
+
for w in sorted(wheels, key=lambda x: x.stat().st_mtime, reverse=True)[:1]:
|
|
522
|
+
size_kb = w.stat().st_size / 1024
|
|
523
|
+
print(f"✅ Built: {w.name} ({size_kb:.0f} KB)")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def cmd_publish(args):
|
|
527
|
+
"""Publish to PyPI (or TestPyPI with --test)."""
|
|
528
|
+
project_dir = Path.cwd()
|
|
529
|
+
config = _load_project(project_dir)
|
|
530
|
+
name = config["name"]
|
|
531
|
+
|
|
532
|
+
dist_dir = project_dir / "dist"
|
|
533
|
+
wheels = sorted(dist_dir.glob("*.whl"), key=lambda x: x.stat().st_mtime, reverse=True)
|
|
534
|
+
|
|
535
|
+
if not wheels:
|
|
536
|
+
print(f"❌ No wheels found in dist/. Run `applepy build` first.")
|
|
537
|
+
sys.exit(1)
|
|
538
|
+
|
|
539
|
+
# Show most recent wheel
|
|
540
|
+
latest = wheels[0]
|
|
541
|
+
print(f"📤 Publishing {name} to {'TestPyPI' if args.test else 'PyPI'}...")
|
|
542
|
+
print(f" Wheel: {latest.name}")
|
|
543
|
+
|
|
544
|
+
if args.test:
|
|
545
|
+
repo_url = "https://test.pypi.org/legacy/"
|
|
546
|
+
else:
|
|
547
|
+
repo_url = None
|
|
548
|
+
|
|
549
|
+
if not args.yes:
|
|
550
|
+
confirm = input("\n Proceed? [y/N] ")
|
|
551
|
+
if confirm.lower() not in ("y", "yes"):
|
|
552
|
+
print(" Cancelled.")
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
cmd = [sys.executable, "-m", "twine", "upload"]
|
|
556
|
+
if repo_url:
|
|
557
|
+
cmd += ["--repository-url", repo_url]
|
|
558
|
+
cmd.append(str(latest))
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
subprocess.check_call(cmd)
|
|
562
|
+
print(f"\n✅ Published {name}!")
|
|
563
|
+
if args.test:
|
|
564
|
+
print(f" pip install -i https://test.pypi.org/simple/ {name}")
|
|
565
|
+
else:
|
|
566
|
+
print(f" pip install {name}")
|
|
567
|
+
except FileNotFoundError:
|
|
568
|
+
print(f"❌ twine not found. Install it: pip install twine")
|
|
569
|
+
sys.exit(1)
|
|
570
|
+
except subprocess.CalledProcessError:
|
|
571
|
+
print(f"❌ Upload failed")
|
|
572
|
+
sys.exit(1)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# ── Main ────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
def main():
|
|
578
|
+
parser = argparse.ArgumentParser(
|
|
579
|
+
prog="applepy",
|
|
580
|
+
description="🍎 ApplePy — Build tool for Swift-powered Python packages",
|
|
581
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
582
|
+
epilog=dedent("""\
|
|
583
|
+
Examples:
|
|
584
|
+
applepy new myproject Create project (GitHub ApplePy)
|
|
585
|
+
applepy new myproject --local Create project (local ApplePy)
|
|
586
|
+
cd myproject && applepy develop Build and install
|
|
587
|
+
applepy build Build a wheel
|
|
588
|
+
applepy publish --test Publish to TestPyPI
|
|
589
|
+
"""),
|
|
590
|
+
)
|
|
591
|
+
parser.add_argument("--version", action="version", version=f"applepy {__version__}")
|
|
592
|
+
sub = parser.add_subparsers(dest="command", help="Command to run")
|
|
593
|
+
|
|
594
|
+
# new
|
|
595
|
+
p_new = sub.add_parser("new", help="Create a new ApplePy project")
|
|
596
|
+
p_new.add_argument("name", help="Project name (e.g. myproject)")
|
|
597
|
+
p_new.add_argument("-d", "--description", help="Project description")
|
|
598
|
+
p_new.add_argument(
|
|
599
|
+
"--local", action="store_true",
|
|
600
|
+
help="Use local ApplePy path instead of GitHub URL (for development)",
|
|
601
|
+
)
|
|
602
|
+
p_new.add_argument(
|
|
603
|
+
"--applepy-path",
|
|
604
|
+
help="Local path to ApplePy package (implies --local, auto-detected if not set)",
|
|
605
|
+
)
|
|
606
|
+
p_new.add_argument(
|
|
607
|
+
"--applepy-url",
|
|
608
|
+
help=f"GitHub URL for ApplePy (default: {APPLEPY_GITHUB_URL})",
|
|
609
|
+
)
|
|
610
|
+
p_new.add_argument(
|
|
611
|
+
"--applepy-version",
|
|
612
|
+
help=f"Minimum ApplePy version (default: {APPLEPY_MIN_VERSION})",
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# develop
|
|
616
|
+
sub.add_parser("develop", help="Build Swift and install into current env")
|
|
617
|
+
|
|
618
|
+
# build
|
|
619
|
+
sub.add_parser("build", help="Build a distributable wheel")
|
|
620
|
+
|
|
621
|
+
# publish
|
|
622
|
+
p_pub = sub.add_parser("publish", help="Publish to PyPI")
|
|
623
|
+
p_pub.add_argument("--test", action="store_true", help="Publish to TestPyPI instead")
|
|
624
|
+
p_pub.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
|
|
625
|
+
|
|
626
|
+
args = parser.parse_args()
|
|
627
|
+
|
|
628
|
+
if not args.command:
|
|
629
|
+
parser.print_help()
|
|
630
|
+
sys.exit(0)
|
|
631
|
+
|
|
632
|
+
# --applepy-path implies --local
|
|
633
|
+
if args.command == "new" and args.applepy_path:
|
|
634
|
+
args.local = True
|
|
635
|
+
|
|
636
|
+
commands = {
|
|
637
|
+
"new": cmd_new,
|
|
638
|
+
"develop": cmd_develop,
|
|
639
|
+
"build": cmd_build,
|
|
640
|
+
"publish": cmd_publish,
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
commands[args.command](args)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
if __name__ == "__main__":
|
|
647
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "applepy-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Build tool for ApplePy — scaffold, build, and publish Swift-powered Python packages"
|
|
9
|
+
license = {text = "BSD-3-Clause"}
|
|
10
|
+
authors = [{name = "Jagtesh Chadha"}]
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Operating System :: MacOS",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Software Development :: Build Tools",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
applepy = "applepy_cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
py-modules = ["applepy_cli"]
|