ndev-stack 0.3.0__tar.gz → 0.3.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.
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/PKG-INFO +2 -1
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/README.md +1 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/__init__.py +1 -1
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/mongodb/module.py +8 -3
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/postgres/module.py +8 -3
- ndev_stack-0.3.1/ndev/win/bin/shim.exe +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/cli.py +78 -4
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/php.py +18 -28
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/redis_core.py +8 -5
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/setup.py +27 -27
- ndev_stack-0.3.1/ndev/win/core/shim.py +284 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/upgrade.py +12 -11
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/PKG-INFO +2 -1
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/SOURCES.txt +4 -1
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/pyproject.toml +2 -2
- ndev_stack-0.3.1/tests/test_shim.py +86 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/__main__.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/cli.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/__init__.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/config.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/constants.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/github.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/logger.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/manifest.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/__init__.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/base.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/mailpit/module.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/redis/manifest.json +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/redis/module.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/manager.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/utils.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/__init__.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/chroot/manager.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/chroot/packages.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/chroot/shell.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/cli.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/available.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/clean.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/ctl.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/current.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/db.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/doctor.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/grok.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/install.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/list.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/logs.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/mailpit.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/modules.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/mongodb.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/postgres.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/reload.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/restart.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/setup.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/start.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/status.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/stop.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/uninstall.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/update.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/upgrade.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/use.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/vhost.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/builder.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/downloader.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/extensions.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/installer.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/resolver.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/templates.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/fpm.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/mailpit.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/pma.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/process.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/sockets.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/upgrade.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/tui.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/main.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/tui.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/__init__.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/commands/__init__.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/__init__.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/db.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/elevate.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/ext.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/fcgi.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/grok.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/logs.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/mailpit.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/mkcert.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/paths.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/pma.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/services.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/vhost.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/templates/vhost.conf.tmpl +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/tui.py +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/dependency_links.txt +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/entry_points.txt +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/requires.txt +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/top_level.txt +0 -0
- {ndev_stack-0.3.0 → ndev_stack-0.3.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
|
|
5
5
|
Author-email: Ankit Karan <ankit.karan99@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankitkaran99/ndev
|
|
@@ -244,6 +244,7 @@ ndev ui
|
|
|
244
244
|
| `ndev upgrade` | Check for and upgrade stack components (Nginx, MariaDB, Redis, PMA, Mailpit, mkcert, Composer) |
|
|
245
245
|
| `ndev upgrade --check` | Check component versions against upstream releases without upgrading |
|
|
246
246
|
| `ndev shell` | Interactive developer subshell pre-loaded with PHP/Composer/MySQL/Redis on PATH |
|
|
247
|
+
| `ndev shim` | Manage Windows binary shims and PATH registration (`list`, `init`, `add`, `rm`) |
|
|
247
248
|
| `ndev doctor` | Run environment diagnostics and health checks |
|
|
248
249
|
| `ndev logs` | View and tail service and virtual host logs |
|
|
249
250
|
| `ndev clean` | Clean up downloads cache and stale runtime state |
|
|
@@ -223,6 +223,7 @@ ndev ui
|
|
|
223
223
|
| `ndev upgrade` | Check for and upgrade stack components (Nginx, MariaDB, Redis, PMA, Mailpit, mkcert, Composer) |
|
|
224
224
|
| `ndev upgrade --check` | Check component versions against upstream releases without upgrading |
|
|
225
225
|
| `ndev shell` | Interactive developer subshell pre-loaded with PHP/Composer/MySQL/Redis on PATH |
|
|
226
|
+
| `ndev shim` | Manage Windows binary shims and PATH registration (`list`, `init`, `add`, `rm`) |
|
|
226
227
|
| `ndev doctor` | Run environment diagnostics and health checks |
|
|
227
228
|
| `ndev logs` | View and tail service and virtual host logs |
|
|
228
229
|
| `ndev clean` | Clean up downloads cache and stale runtime state |
|
|
@@ -119,16 +119,21 @@ class Module:
|
|
|
119
119
|
except Exception:
|
|
120
120
|
pass
|
|
121
121
|
|
|
122
|
-
#
|
|
122
|
+
# Create tool shims in ~/.ndev/shims/
|
|
123
123
|
shim_dir = self.module_dir.parent.parent / "shims"
|
|
124
124
|
if shim_dir.exists():
|
|
125
125
|
for tool in ["mongod.exe", "mongos.exe", "mongosh.exe"]:
|
|
126
126
|
src_tool = self.bin_dir / tool
|
|
127
127
|
if src_tool.exists():
|
|
128
|
+
name = tool[:-4] if tool.lower().endswith(".exe") else tool
|
|
128
129
|
try:
|
|
129
|
-
|
|
130
|
+
from ndev.win.core import shim as win_shim
|
|
131
|
+
win_shim.create(name, str(src_tool), path_prepend=str(self.bin_dir), shim_dir=shim_dir)
|
|
130
132
|
except Exception:
|
|
131
|
-
|
|
133
|
+
try:
|
|
134
|
+
shutil.copy2(src_tool, shim_dir / tool)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
132
137
|
else:
|
|
133
138
|
# Linux: try installing via system package manager or apt
|
|
134
139
|
if not shutil.which("mongod"):
|
|
@@ -108,16 +108,21 @@ class Module:
|
|
|
108
108
|
except Exception:
|
|
109
109
|
pass
|
|
110
110
|
|
|
111
|
-
#
|
|
111
|
+
# Create key cli binary shims in ~/.ndev/shims/
|
|
112
112
|
shim_dir = self.module_dir.parent.parent / "shims"
|
|
113
113
|
if shim_dir.exists():
|
|
114
114
|
for tool in ["psql.exe", "pg_ctl.exe", "pg_dump.exe", "createdb.exe", "dropdb.exe"]:
|
|
115
115
|
src_tool = self.bin_dir / tool
|
|
116
116
|
if src_tool.exists():
|
|
117
|
+
name = tool[:-4] if tool.lower().endswith(".exe") else tool
|
|
117
118
|
try:
|
|
118
|
-
|
|
119
|
+
from ndev.win.core import shim as win_shim
|
|
120
|
+
win_shim.create(name, str(src_tool), path_prepend=str(self.bin_dir), shim_dir=shim_dir)
|
|
119
121
|
except Exception:
|
|
120
|
-
|
|
122
|
+
try:
|
|
123
|
+
shutil.copy2(src_tool, shim_dir / tool)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
121
126
|
else:
|
|
122
127
|
# Linux: try installing via system package manager
|
|
123
128
|
if not shutil.which("pg_ctl"):
|
|
Binary file
|
|
@@ -33,6 +33,7 @@ from .core import (
|
|
|
33
33
|
redis_core,
|
|
34
34
|
services,
|
|
35
35
|
setup as setup_core,
|
|
36
|
+
shim,
|
|
36
37
|
upgrade as upgrade_core,
|
|
37
38
|
vhost as vhost_core,
|
|
38
39
|
)
|
|
@@ -375,8 +376,8 @@ def doctor():
|
|
|
375
376
|
table.add_row("ngrok Tunneling", "[yellow]MISSING[/yellow]", "Run `ndev setup` to install")
|
|
376
377
|
|
|
377
378
|
# Composer
|
|
378
|
-
|
|
379
|
-
if
|
|
379
|
+
composer_cmd = paths.SHIM_DIR / "composer.exe"
|
|
380
|
+
if composer_cmd.exists() or shutil.which("composer"):
|
|
380
381
|
table.add_row("Composer", "[green]OK[/green]", "Installed and available on CLI")
|
|
381
382
|
else:
|
|
382
383
|
table.add_row("Composer", "[yellow]MISSING[/yellow]", "Run `ndev setup` to install")
|
|
@@ -390,7 +391,7 @@ def doctor():
|
|
|
390
391
|
# PATH Check
|
|
391
392
|
shim_on_path = str(paths.SHIM_DIR).lower() in os.environ.get("PATH", "").lower()
|
|
392
393
|
path_status = "[green]OK[/green]" if shim_on_path else "[yellow]WARNING[/yellow]"
|
|
393
|
-
path_detail = f"Found on PATH ({paths.SHIM_DIR})" if shim_on_path else f"
|
|
394
|
+
path_detail = f"Found on PATH ({paths.SHIM_DIR})" if shim_on_path else f"Run `ndev shim init` or add {paths.SHIM_DIR} to PATH"
|
|
394
395
|
table.add_row("Shims on PATH", path_status, path_detail)
|
|
395
396
|
|
|
396
397
|
console.print(table)
|
|
@@ -1994,9 +1995,10 @@ def setup(nginx, mariadb, mkcert, ngrok, composer, cacert, nginx_version, mariad
|
|
|
1994
1995
|
for name, path in results.items():
|
|
1995
1996
|
console.print(f"[bold green]✓[/bold green] Installed {name:<10} -> {path}")
|
|
1996
1997
|
|
|
1998
|
+
shim.ensure_on_path()
|
|
1997
1999
|
shim_on_path = str(paths.SHIM_DIR).lower() in os.environ.get("PATH", "").lower()
|
|
1998
2000
|
if not shim_on_path:
|
|
1999
|
-
console.print(f"\n[bold
|
|
2001
|
+
console.print(f"\n[bold green]✓[/bold green] Added [bold cyan]{paths.SHIM_DIR}[/bold cyan] to User PATH (open a new terminal to use shims).")
|
|
2000
2002
|
|
|
2001
2003
|
|
|
2002
2004
|
# ---- Component Upgrade (upgrade) --------------------------------------------
|
|
@@ -2372,6 +2374,78 @@ def mongodb_status_cmd():
|
|
|
2372
2374
|
console.print(f"MongoDB: {'[bold green]RUNNING[/bold green]' if st.get('running') else '[bold red]STOPPED[/bold red]'} ({st.get('details', '')})")
|
|
2373
2375
|
|
|
2374
2376
|
|
|
2377
|
+
# ---- Shim Management (shim / shims) -----------------------------------------
|
|
2378
|
+
|
|
2379
|
+
@main.group(name="shim")
|
|
2380
|
+
def shim_cmd():
|
|
2381
|
+
"""Manage Windows executable shims and PATH registration."""
|
|
2382
|
+
pass
|
|
2383
|
+
|
|
2384
|
+
|
|
2385
|
+
@shim_cmd.command(name="list")
|
|
2386
|
+
def shim_list_cmd():
|
|
2387
|
+
"""List all configured shims."""
|
|
2388
|
+
shims = shim.list_shims()
|
|
2389
|
+
if not shims:
|
|
2390
|
+
console.print("[yellow]No shims configured in ~/.ndev/shims.[/yellow]")
|
|
2391
|
+
return
|
|
2392
|
+
|
|
2393
|
+
table = Table(title="ndev Windows Shims")
|
|
2394
|
+
table.add_column("Command", style="bold cyan")
|
|
2395
|
+
table.add_column("Target Executable / Details", style="green")
|
|
2396
|
+
for name, target in shims.items():
|
|
2397
|
+
table.add_row(name, target)
|
|
2398
|
+
console.print(table)
|
|
2399
|
+
|
|
2400
|
+
|
|
2401
|
+
@shim_cmd.command(name="init")
|
|
2402
|
+
def shim_init_cmd():
|
|
2403
|
+
"""Ensure ~/.ndev/shims is added to the user PATH environment variable."""
|
|
2404
|
+
updated = shim.ensure_on_path()
|
|
2405
|
+
if updated:
|
|
2406
|
+
console.print(f"[bold green]✓ Added {paths.SHIM_DIR} to User PATH registry.[/bold green]")
|
|
2407
|
+
console.print("[dim]A WM_SETTINGCHANGE broadcast was sent. Open a new terminal to use shims.[/dim]")
|
|
2408
|
+
else:
|
|
2409
|
+
console.print(f"[green]✓ {paths.SHIM_DIR} is already present in your User PATH registry.[/green]")
|
|
2410
|
+
|
|
2411
|
+
|
|
2412
|
+
@shim_cmd.command(name="add")
|
|
2413
|
+
@click.argument("name")
|
|
2414
|
+
@click.argument("target")
|
|
2415
|
+
@click.option("--args", default="", help="Default arguments to pass to target executable")
|
|
2416
|
+
@click.option("--cwd", default=None, help="Working directory for execution")
|
|
2417
|
+
@click.option("--prepend", default=None, help="Directory to prepend to PATH for target")
|
|
2418
|
+
def shim_add_cmd(name, target, args, cwd, prepend):
|
|
2419
|
+
"""Create or update a custom native Windows .exe shim."""
|
|
2420
|
+
try:
|
|
2421
|
+
created = shim.create(name, target, args=args, cwd=cwd, path_prepend=prepend)
|
|
2422
|
+
console.print(f"[bold green]✓ Created shim '{name}' -> {created}[/bold green]")
|
|
2423
|
+
except Exception as e:
|
|
2424
|
+
raise click.ClickException(str(e))
|
|
2425
|
+
|
|
2426
|
+
|
|
2427
|
+
@shim_cmd.command(name="rm")
|
|
2428
|
+
@click.argument("name")
|
|
2429
|
+
def shim_rm_cmd(name):
|
|
2430
|
+
"""Remove a shim."""
|
|
2431
|
+
if shim.remove(name):
|
|
2432
|
+
console.print(f"[bold green]✓ Removed shim '{name}'.[/bold green]")
|
|
2433
|
+
else:
|
|
2434
|
+
console.print(f"[yellow]No shim named '{name}' found.[/yellow]")
|
|
2435
|
+
|
|
2436
|
+
|
|
2437
|
+
@main.group(name="shims", hidden=True)
|
|
2438
|
+
def shims_alias():
|
|
2439
|
+
"""Alias for shim."""
|
|
2440
|
+
pass
|
|
2441
|
+
|
|
2442
|
+
|
|
2443
|
+
shims_alias.add_command(shim_list_cmd, "list")
|
|
2444
|
+
shims_alias.add_command(shim_init_cmd, "init")
|
|
2445
|
+
shims_alias.add_command(shim_add_cmd, "add")
|
|
2446
|
+
shims_alias.add_command(shim_rm_cmd, "rm")
|
|
2447
|
+
|
|
2448
|
+
|
|
2375
2449
|
if __name__ == "__main__":
|
|
2376
2450
|
sys.exit(main())
|
|
2377
2451
|
|
|
@@ -15,7 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
import httpx
|
|
17
17
|
|
|
18
|
-
from . import paths
|
|
18
|
+
from . import paths, shim
|
|
19
19
|
|
|
20
20
|
RELEASES_INDEX_URL = "https://windows.php.net/downloads/releases/releases.json"
|
|
21
21
|
ARCHIVES_INDEX_URL = "https://windows.php.net/downloads/releases/archives/"
|
|
@@ -498,12 +498,9 @@ def uninstall(version: str) -> None:
|
|
|
498
498
|
if paths.CURRENT_FILE.exists():
|
|
499
499
|
paths.CURRENT_FILE.unlink(missing_ok=True)
|
|
500
500
|
# Remove all PHP and Composer shims
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
"composer.bat", "composer.cmd", "composer.ps1",
|
|
505
|
-
]:
|
|
506
|
-
(paths.SHIM_DIR / shim).unlink(missing_ok=True)
|
|
501
|
+
shim.remove("php")
|
|
502
|
+
shim.remove("php-cgi")
|
|
503
|
+
shim.remove("composer")
|
|
507
504
|
|
|
508
505
|
|
|
509
506
|
def list_installed() -> list[str]:
|
|
@@ -570,37 +567,30 @@ def get_current_version() -> str | None:
|
|
|
570
567
|
def use(version: str) -> str:
|
|
571
568
|
"""
|
|
572
569
|
Point ndev shims (on PATH) at this version's php.exe and php-cgi.exe.
|
|
573
|
-
Generates
|
|
570
|
+
Generates binary .exe shims with .cmd fallback in ~/.ndev/shims.
|
|
574
571
|
Returns the resolved version string.
|
|
575
572
|
"""
|
|
576
573
|
resolved_ver = resolve_installed(version)
|
|
577
574
|
paths.ensure_dirs()
|
|
578
575
|
|
|
579
|
-
|
|
580
|
-
|
|
576
|
+
php_dir = paths.version_dir(resolved_ver)
|
|
577
|
+
exe_path = str(php_dir / "php.exe")
|
|
578
|
+
cgi_path = str(php_dir / "php-cgi.exe")
|
|
581
579
|
|
|
582
|
-
#
|
|
583
|
-
(
|
|
584
|
-
(
|
|
585
|
-
(paths.SHIM_DIR / "php.ps1").write_text(f'& "{exe_path}" @args\r\n', encoding="utf-8")
|
|
580
|
+
# Create native shims via shimgen with path_prepend for PHP DLLs (cleans any legacy scripts)
|
|
581
|
+
shim.create("php", exe_path, path_prepend=str(php_dir), clean_legacy=True)
|
|
582
|
+
shim.create("php-cgi", cgi_path, path_prepend=str(php_dir), clean_legacy=True)
|
|
586
583
|
|
|
587
|
-
#
|
|
588
|
-
(paths.SHIM_DIR / "php-cgi.bat").write_text(f'@echo off\r\n"{cgi_path}" %*\r\n', encoding="utf-8")
|
|
589
|
-
(paths.SHIM_DIR / "php-cgi.cmd").write_text(f'@echo off\r\n"{cgi_path}" %*\r\n', encoding="utf-8")
|
|
590
|
-
(paths.SHIM_DIR / "php-cgi.ps1").write_text(f'& "{cgi_path}" @args\r\n', encoding="utf-8")
|
|
591
|
-
|
|
592
|
-
# If composer.phar is present, create/update composer shims
|
|
584
|
+
# If composer.phar is present, create/update composer native shim
|
|
593
585
|
composer_phar = paths.SHIM_DIR / "composer.phar"
|
|
594
586
|
if composer_phar.exists():
|
|
595
587
|
clean_phar = str(composer_phar.resolve()).replace("\\", "/")
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
(paths.SHIM_DIR / "composer.ps1").write_text(
|
|
603
|
-
f'& "{exe_path}" "{clean_phar}" @args\r\n', encoding="utf-8"
|
|
588
|
+
shim.create(
|
|
589
|
+
"composer",
|
|
590
|
+
exe_path,
|
|
591
|
+
args=f'"{clean_phar}"',
|
|
592
|
+
path_prepend=str(php_dir),
|
|
593
|
+
clean_legacy=True,
|
|
604
594
|
)
|
|
605
595
|
|
|
606
596
|
paths.set_current_version(resolved_ver)
|
|
@@ -16,7 +16,7 @@ import time
|
|
|
16
16
|
import zipfile
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
-
from . import fcgi, paths, setup as setup_core
|
|
19
|
+
from . import fcgi, paths, setup as setup_core, shim
|
|
20
20
|
|
|
21
21
|
DEFAULT_PORT = 6379
|
|
22
22
|
DEFAULT_VERSION = "8.10.1"
|
|
@@ -103,12 +103,15 @@ def install(version: str = DEFAULT_VERSION) -> Path:
|
|
|
103
103
|
)
|
|
104
104
|
|
|
105
105
|
# Place redis-cli shim in ~/.ndev/shims/
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
cli_file = REDIS_DIR / "redis-cli.exe"
|
|
107
|
+
if cli_file.exists():
|
|
108
108
|
try:
|
|
109
|
-
|
|
109
|
+
shim.create("redis-cli", str(cli_file), path_prepend=str(REDIS_DIR))
|
|
110
110
|
except Exception:
|
|
111
|
-
|
|
111
|
+
try:
|
|
112
|
+
shutil.copy2(cli_file, paths.SHIM_DIR / "redis-cli.exe")
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
112
115
|
|
|
113
116
|
return REDIS_DIR
|
|
114
117
|
|
|
@@ -12,7 +12,7 @@ from typing import Optional
|
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
14
|
|
|
15
|
-
from . import paths
|
|
15
|
+
from . import paths, shim
|
|
16
16
|
|
|
17
17
|
# ---- Defaults ---------------------------------------------------------------
|
|
18
18
|
|
|
@@ -170,16 +170,13 @@ def install_mariadb(version: str = DEFAULT_MARIADB_VERSION) -> Path:
|
|
|
170
170
|
|
|
171
171
|
def _create_mariadb_shims() -> None:
|
|
172
172
|
paths.ensure_dirs()
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
bin_dir = paths.MARIADB_DIR / "bin"
|
|
174
|
+
mysql_exe = bin_dir / "mysql.exe"
|
|
175
|
+
mysqldump_exe = bin_dir / "mysqldump.exe"
|
|
175
176
|
if mysql_exe.exists():
|
|
176
|
-
(
|
|
177
|
-
(paths.SHIM_DIR / "mysql.cmd").write_text(f'@echo off\r\n"{mysql_exe}" %*\r\n', encoding="utf-8")
|
|
178
|
-
(paths.SHIM_DIR / "mysql.ps1").write_text(f'& "{mysql_exe}" @args\r\n', encoding="utf-8")
|
|
177
|
+
shim.create("mysql", str(mysql_exe), path_prepend=str(bin_dir), clean_legacy=True)
|
|
179
178
|
if mysqldump_exe.exists():
|
|
180
|
-
(
|
|
181
|
-
(paths.SHIM_DIR / "mysqldump.cmd").write_text(f'@echo off\r\n"{mysqldump_exe}" %*\r\n', encoding="utf-8")
|
|
182
|
-
(paths.SHIM_DIR / "mysqldump.ps1").write_text(f'& "{mysqldump_exe}" @args\r\n', encoding="utf-8")
|
|
179
|
+
shim.create("mysqldump", str(mysqldump_exe), path_prepend=str(bin_dir), clean_legacy=True)
|
|
183
180
|
|
|
184
181
|
|
|
185
182
|
def _init_mariadb_data_dir() -> None:
|
|
@@ -280,14 +277,28 @@ def install_ngrok() -> Path:
|
|
|
280
277
|
# ---- Composer -----------------------------------------------------------------
|
|
281
278
|
|
|
282
279
|
def install_composer() -> Path:
|
|
283
|
-
"""Download composer.phar and generate
|
|
280
|
+
"""Download composer.phar and generate native composer.exe shim."""
|
|
284
281
|
phar_dest = paths.SHIM_DIR / "composer.phar"
|
|
285
282
|
_download(COMPOSER_PHAR_URL, phar_dest)
|
|
286
283
|
|
|
287
284
|
clean_phar = str(phar_dest.resolve()).replace("\\", "/")
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
# If active PHP is installed, point composer shim directly to it
|
|
286
|
+
from . import php
|
|
287
|
+
curr_ver = php.get_current_version()
|
|
288
|
+
if curr_ver:
|
|
289
|
+
try:
|
|
290
|
+
exe_path = str(paths.version_dir(curr_ver) / "php.exe")
|
|
291
|
+
shim.create(
|
|
292
|
+
"composer",
|
|
293
|
+
exe_path,
|
|
294
|
+
args=f'"{clean_phar}"',
|
|
295
|
+
path_prepend=str(paths.version_dir(curr_ver)),
|
|
296
|
+
clean_legacy=True,
|
|
297
|
+
)
|
|
298
|
+
return phar_dest
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
|
|
291
302
|
return phar_dest
|
|
292
303
|
|
|
293
304
|
|
|
@@ -306,25 +317,14 @@ def install_cacert() -> Path:
|
|
|
306
317
|
|
|
307
318
|
|
|
308
319
|
def create_ndev_shims() -> Path:
|
|
309
|
-
"""Create ndev
|
|
320
|
+
"""Create ndev native executable shim in ~/.ndev/shims/ pointing to active python/ndev."""
|
|
310
321
|
import sys
|
|
311
322
|
paths.ensure_dirs()
|
|
312
323
|
scripts_dir = Path(sys.executable).parent
|
|
313
324
|
ndev_exe = scripts_dir / "ndev.exe"
|
|
314
325
|
if ndev_exe.exists():
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
else:
|
|
318
|
-
bat_target = f'"{sys.executable}" -m ndev_win.cli %*'
|
|
319
|
-
ps_target = f'& "{sys.executable}" -m ndev_win.cli @args'
|
|
320
|
-
|
|
321
|
-
bat_path = paths.SHIM_DIR / "ndev.bat"
|
|
322
|
-
cmd_path = paths.SHIM_DIR / "ndev.cmd"
|
|
323
|
-
ps_path = paths.SHIM_DIR / "ndev.ps1"
|
|
324
|
-
bat_path.write_text(f'@echo off\r\n{bat_target}\r\n', encoding="utf-8")
|
|
325
|
-
cmd_path.write_text(f'@echo off\r\n{bat_target}\r\n', encoding="utf-8")
|
|
326
|
-
ps_path.write_text(f'{ps_target}\r\n', encoding="utf-8")
|
|
327
|
-
return bat_path
|
|
326
|
+
return shim.create("ndev", str(ndev_exe), clean_legacy=True)
|
|
327
|
+
return shim.create("ndev", sys.executable, args="-m ndev.main", clean_legacy=True)
|
|
328
328
|
|
|
329
329
|
|
|
330
330
|
# ---- Orchestration ----------------------------------------------------------
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
shim generator and manager for ndev on Windows.
|
|
3
|
+
|
|
4
|
+
Provides native Windows .exe shims using shim.exe + <name>.shim config pairs.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import filecmp
|
|
9
|
+
import ntpath
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
import warnings
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import paths
|
|
18
|
+
|
|
19
|
+
# By default, use ndev's central SHIM_DIR
|
|
20
|
+
SHIM_DIR = paths.SHIM_DIR
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _find_shim_exe() -> Path:
|
|
24
|
+
"""Find prebuilt shim.exe binary across installed package or repository."""
|
|
25
|
+
# 1. Check explicit environment override
|
|
26
|
+
env_exe = os.environ.get("NDEV_SHIM_EXE")
|
|
27
|
+
if env_exe:
|
|
28
|
+
p = Path(env_exe)
|
|
29
|
+
if p.exists():
|
|
30
|
+
return p
|
|
31
|
+
|
|
32
|
+
# 2. Check bundled package binary (ndev/win/bin/shim.exe)
|
|
33
|
+
pkg_exe = Path(__file__).parent.parent / "bin" / "shim.exe"
|
|
34
|
+
if pkg_exe.exists():
|
|
35
|
+
return pkg_exe
|
|
36
|
+
|
|
37
|
+
# 3. Check development repository root (shimgen/shim.exe)
|
|
38
|
+
repo_exe = Path(__file__).resolve().parent.parent.parent.parent / "shimgen" / "shim.exe"
|
|
39
|
+
if repo_exe.exists():
|
|
40
|
+
return repo_exe
|
|
41
|
+
|
|
42
|
+
return pkg_exe
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
SHIM_EXE = _find_shim_exe()
|
|
46
|
+
|
|
47
|
+
_NAME_RE = re.compile(r"^[\w.+\-]+$")
|
|
48
|
+
|
|
49
|
+
_RESERVED = {
|
|
50
|
+
"con", "prn", "aux", "nul",
|
|
51
|
+
*(f"com{i}" for i in range(1, 10)),
|
|
52
|
+
*(f"lpt{i}" for i in range(1, 10)),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _check_name(name: str) -> str:
|
|
57
|
+
if (
|
|
58
|
+
not _NAME_RE.match(name)
|
|
59
|
+
or name.endswith(".")
|
|
60
|
+
or name.split(".")[0].lower() in _RESERVED
|
|
61
|
+
):
|
|
62
|
+
raise ValueError(f"invalid shim name: {name!r}")
|
|
63
|
+
return name
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _write(path: Path, text: str, encoding: str = "utf-8") -> None:
|
|
67
|
+
"""Atomic write: a shim starting mid-update never sees a half-written file."""
|
|
68
|
+
tmp = path.with_name(f"{path.name}.tmp-{os.getpid()}")
|
|
69
|
+
with open(tmp, "w", encoding=encoding, newline="\r\n") as f:
|
|
70
|
+
f.write(text)
|
|
71
|
+
try:
|
|
72
|
+
os.replace(tmp, path)
|
|
73
|
+
except OSError:
|
|
74
|
+
# Fallback if target is momentarily locked
|
|
75
|
+
try:
|
|
76
|
+
if path.exists():
|
|
77
|
+
_unlink(path)
|
|
78
|
+
os.replace(tmp, path)
|
|
79
|
+
except OSError:
|
|
80
|
+
try:
|
|
81
|
+
tmp.unlink(missing_ok=True)
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _unlink(p: Path) -> bool:
|
|
88
|
+
"""Delete p. A running .exe can't be deleted on Windows but can be renamed,
|
|
89
|
+
so rename it out of the way and sweep it later."""
|
|
90
|
+
if not p.exists():
|
|
91
|
+
return False
|
|
92
|
+
try:
|
|
93
|
+
p.unlink()
|
|
94
|
+
except PermissionError:
|
|
95
|
+
try:
|
|
96
|
+
os.replace(p, p.with_name(f"{p.name}.old-{os.getpid()}"))
|
|
97
|
+
except OSError:
|
|
98
|
+
pass
|
|
99
|
+
except OSError:
|
|
100
|
+
return False
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _sweep(shim_dir: Path) -> None:
|
|
105
|
+
for p in shim_dir.glob("*.old-*"):
|
|
106
|
+
try:
|
|
107
|
+
p.unlink()
|
|
108
|
+
except OSError:
|
|
109
|
+
pass # still running; sweep next time
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _same(a: Path, b: Path) -> bool:
|
|
113
|
+
try:
|
|
114
|
+
return os.path.samefile(a, b) or filecmp.cmp(a, b, shallow=False)
|
|
115
|
+
except OSError:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _install_exe(dst: Path) -> None:
|
|
120
|
+
if dst.exists():
|
|
121
|
+
if _same(SHIM_EXE, dst):
|
|
122
|
+
return # already current (also avoids touching an exe that is running)
|
|
123
|
+
try:
|
|
124
|
+
os.replace(dst, dst.with_name(f"{dst.name}.old-{os.getpid()}"))
|
|
125
|
+
except OSError:
|
|
126
|
+
pass
|
|
127
|
+
try:
|
|
128
|
+
os.link(SHIM_EXE, dst) # hard link: fast, zero disk copy overhead
|
|
129
|
+
except OSError:
|
|
130
|
+
shutil.copyfile(SHIM_EXE, dst) # cross-volume or non-NTFS fallback
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def create(
|
|
134
|
+
name: str,
|
|
135
|
+
target: str | Path,
|
|
136
|
+
args: str = "",
|
|
137
|
+
cwd: str | Path | None = None,
|
|
138
|
+
env: dict[str, str] | None = None,
|
|
139
|
+
path_prepend: str | Path | None = None,
|
|
140
|
+
shim_dir: Path | None = None,
|
|
141
|
+
clean_legacy: bool = True,
|
|
142
|
+
) -> Path:
|
|
143
|
+
"""Create or update a native Windows .exe shim in shim_dir (defaults to ~/.ndev/shims).
|
|
144
|
+
|
|
145
|
+
Returns the path to the generated <shim_dir>/<name>.exe.
|
|
146
|
+
"""
|
|
147
|
+
_check_name(name)
|
|
148
|
+
shim_dir = Path(shim_dir or SHIM_DIR)
|
|
149
|
+
shim_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
target = str(target)
|
|
151
|
+
env = env or {}
|
|
152
|
+
|
|
153
|
+
if not (ntpath.isabs(target) or target.startswith("%")):
|
|
154
|
+
raise ValueError(f"shim target must be an absolute Windows path, got {target!r}")
|
|
155
|
+
if not os.path.isfile(os.path.expandvars(target)):
|
|
156
|
+
warnings.warn(f"shim target does not exist (yet): {target}", stacklevel=2)
|
|
157
|
+
|
|
158
|
+
_sweep(shim_dir)
|
|
159
|
+
|
|
160
|
+
if not SHIM_EXE.is_file():
|
|
161
|
+
raise FileNotFoundError(f"shim.exe not found at {SHIM_EXE}; build or place shim.exe first")
|
|
162
|
+
|
|
163
|
+
lines = [f"path = {target}"]
|
|
164
|
+
if args:
|
|
165
|
+
lines.append(f"args = {args}")
|
|
166
|
+
if cwd:
|
|
167
|
+
lines.append(f"cwd = {cwd}")
|
|
168
|
+
if path_prepend:
|
|
169
|
+
lines.append(f"path_prepend = {path_prepend}")
|
|
170
|
+
lines += [f"env.{k} = {v}" for k, v in sorted(env.items())]
|
|
171
|
+
|
|
172
|
+
# Write config first, exe second: the exe never exists without its config
|
|
173
|
+
_write(shim_dir / f"{name}.shim", "\n".join(lines) + "\n")
|
|
174
|
+
_install_exe(shim_dir / f"{name}.exe")
|
|
175
|
+
|
|
176
|
+
# Clean legacy script shims (.cmd, .bat, .ps1)
|
|
177
|
+
if clean_legacy:
|
|
178
|
+
_unlink(shim_dir / f"{name}.cmd")
|
|
179
|
+
_unlink(shim_dir / f"{name}.bat")
|
|
180
|
+
_unlink(shim_dir / f"{name}.ps1")
|
|
181
|
+
|
|
182
|
+
return shim_dir / f"{name}.exe"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def remove(name: str, shim_dir: Path | None = None) -> bool:
|
|
186
|
+
"""Remove all shims and configs for `name` (.exe, .shim, .cmd, .bat, .ps1)."""
|
|
187
|
+
_check_name(name)
|
|
188
|
+
shim_dir = Path(shim_dir or SHIM_DIR)
|
|
189
|
+
removed = False
|
|
190
|
+
for ext in (".exe", ".shim", ".cmd", ".bat", ".ps1"):
|
|
191
|
+
removed |= _unlink(shim_dir / f"{name}{ext}")
|
|
192
|
+
return removed
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def read_shim(name: str, shim_dir: Path | None = None) -> dict[str, str]:
|
|
196
|
+
"""Read a .shim config file into a dictionary."""
|
|
197
|
+
shim_dir = Path(shim_dir or SHIM_DIR)
|
|
198
|
+
cfg: dict[str, str] = {}
|
|
199
|
+
p = shim_dir / f"{name}.shim"
|
|
200
|
+
if p.exists():
|
|
201
|
+
try:
|
|
202
|
+
for line in p.read_text(encoding="utf-8-sig").splitlines():
|
|
203
|
+
line = line.strip()
|
|
204
|
+
if line and not line.startswith("#") and "=" in line:
|
|
205
|
+
k, v = line.split("=", 1)
|
|
206
|
+
cfg[k.strip()] = v.strip()
|
|
207
|
+
except OSError:
|
|
208
|
+
pass
|
|
209
|
+
return cfg
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def list_shims(shim_dir: Path | None = None) -> dict[str, str]:
|
|
213
|
+
"""Return dict of {name: target_path_or_description} for all native shims."""
|
|
214
|
+
shim_dir = Path(shim_dir or SHIM_DIR)
|
|
215
|
+
if not shim_dir.is_dir():
|
|
216
|
+
return {}
|
|
217
|
+
out: dict[str, str] = {}
|
|
218
|
+
|
|
219
|
+
for p in sorted(shim_dir.iterdir()):
|
|
220
|
+
if p.suffix.lower() == ".exe" and (shim_dir / f"{p.stem}.shim").exists():
|
|
221
|
+
cfg = read_shim(p.stem, shim_dir)
|
|
222
|
+
t = cfg.get("path", "?")
|
|
223
|
+
out[p.stem] = (
|
|
224
|
+
t
|
|
225
|
+
if os.path.isfile(os.path.expandvars(t)) or sys.platform != "win32"
|
|
226
|
+
else f"{t} [MISSING]"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def ensure_on_path(shim_dir: Path | None = None) -> bool:
|
|
233
|
+
"""Add the shim directory to the HKCU user PATH registry if not already present.
|
|
234
|
+
Normalizes slashes/case, skips duplicate additions, and broadcasts WM_SETTINGCHANGE.
|
|
235
|
+
Returns True if user PATH was updated, False if already present.
|
|
236
|
+
"""
|
|
237
|
+
if sys.platform != "win32":
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
import ctypes
|
|
241
|
+
import winreg
|
|
242
|
+
from ctypes import wintypes
|
|
243
|
+
|
|
244
|
+
shim_dir_str = str(Path(shim_dir or SHIM_DIR).resolve())
|
|
245
|
+
|
|
246
|
+
def norm(p: str) -> str:
|
|
247
|
+
return os.path.normcase(os.path.normpath(os.path.expandvars(p.strip().rstrip("\\/"))))
|
|
248
|
+
|
|
249
|
+
norm_target = norm(shim_dir_str)
|
|
250
|
+
|
|
251
|
+
with winreg.OpenKey(
|
|
252
|
+
winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_READ | winreg.KEY_WRITE
|
|
253
|
+
) as key:
|
|
254
|
+
try:
|
|
255
|
+
cur, typ = winreg.QueryValueEx(key, "Path")
|
|
256
|
+
except FileNotFoundError:
|
|
257
|
+
cur, typ = "", winreg.REG_EXPAND_SZ
|
|
258
|
+
|
|
259
|
+
parts = [p.strip() for p in cur.split(";") if p.strip()]
|
|
260
|
+
if any(norm(p) == norm_target for p in parts):
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
# Prepend to front so ndev shims take precedence
|
|
264
|
+
parts.insert(0, shim_dir_str)
|
|
265
|
+
winreg.SetValueEx(key, "Path", 0, typ, ";".join(parts))
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
send = ctypes.windll.user32.SendMessageTimeoutW
|
|
269
|
+
send.argtypes = [
|
|
270
|
+
wintypes.HWND,
|
|
271
|
+
wintypes.UINT,
|
|
272
|
+
wintypes.WPARAM,
|
|
273
|
+
wintypes.LPCWSTR,
|
|
274
|
+
wintypes.UINT,
|
|
275
|
+
wintypes.UINT,
|
|
276
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
277
|
+
]
|
|
278
|
+
send.restype = wintypes.LPARAM
|
|
279
|
+
result = wintypes.DWORD()
|
|
280
|
+
send(0xFFFF, 0x001A, 0, "Environment", 2, 5000, ctypes.byref(result)) # HWND_BROADCAST, WM_SETTINGCHANGE
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
return True
|
|
@@ -433,14 +433,15 @@ def get_composer_info() -> ComponentInfo:
|
|
|
433
433
|
|
|
434
434
|
if installed:
|
|
435
435
|
try:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
436
|
+
comp_cmd = paths.SHIM_DIR / "composer.exe"
|
|
437
|
+
if comp_cmd.exists():
|
|
438
|
+
res = subprocess.run([str(comp_cmd), "--version", "--no-ansi"], capture_output=True, text=True, timeout=10)
|
|
439
|
+
raw = (res.stdout or "") + "\n" + (res.stderr or "")
|
|
440
|
+
# Strip ANSI escape sequences if any exist
|
|
441
|
+
clean_text = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", raw)
|
|
442
|
+
m = re.search(r"Composer\s+(?:version\s+)?(\d+\.\d+\.\d+)", clean_text, re.IGNORECASE)
|
|
443
|
+
if m:
|
|
444
|
+
curr_ver = m.group(1)
|
|
444
445
|
except Exception as e:
|
|
445
446
|
err = str(e)
|
|
446
447
|
|
|
@@ -469,9 +470,9 @@ def get_composer_info() -> ComponentInfo:
|
|
|
469
470
|
|
|
470
471
|
def upgrade_composer() -> tuple[bool, str]:
|
|
471
472
|
try:
|
|
472
|
-
|
|
473
|
-
if
|
|
474
|
-
res = subprocess.run([str(
|
|
473
|
+
comp_cmd = paths.SHIM_DIR / "composer.exe"
|
|
474
|
+
if comp_cmd.exists():
|
|
475
|
+
res = subprocess.run([str(comp_cmd), "self-update"], capture_output=True, text=True, timeout=60)
|
|
475
476
|
if res.returncode == 0:
|
|
476
477
|
info = get_composer_info()
|
|
477
478
|
return True, f"Composer upgraded successfully ({info.current_version})."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
|
|
5
5
|
Author-email: Ankit Karan <ankit.karan99@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankitkaran99/ndev
|
|
@@ -244,6 +244,7 @@ ndev ui
|
|
|
244
244
|
| `ndev upgrade` | Check for and upgrade stack components (Nginx, MariaDB, Redis, PMA, Mailpit, mkcert, Composer) |
|
|
245
245
|
| `ndev upgrade --check` | Check component versions against upstream releases without upgrading |
|
|
246
246
|
| `ndev shell` | Interactive developer subshell pre-loaded with PHP/Composer/MySQL/Redis on PATH |
|
|
247
|
+
| `ndev shim` | Manage Windows binary shims and PATH registration (`list`, `init`, `add`, `rm`) |
|
|
247
248
|
| `ndev doctor` | Run environment diagnostics and health checks |
|
|
248
249
|
| `ndev logs` | View and tail service and virtual host logs |
|
|
249
250
|
| `ndev clean` | Clean up downloads cache and stale runtime state |
|
|
@@ -69,6 +69,7 @@ ndev/linux/runtime/upgrade.py
|
|
|
69
69
|
ndev/win/__init__.py
|
|
70
70
|
ndev/win/cli.py
|
|
71
71
|
ndev/win/tui.py
|
|
72
|
+
ndev/win/bin/shim.exe
|
|
72
73
|
ndev/win/commands/__init__.py
|
|
73
74
|
ndev/win/core/__init__.py
|
|
74
75
|
ndev/win/core/db.py
|
|
@@ -85,6 +86,7 @@ ndev/win/core/pma.py
|
|
|
85
86
|
ndev/win/core/redis_core.py
|
|
86
87
|
ndev/win/core/services.py
|
|
87
88
|
ndev/win/core/setup.py
|
|
89
|
+
ndev/win/core/shim.py
|
|
88
90
|
ndev/win/core/upgrade.py
|
|
89
91
|
ndev/win/core/vhost.py
|
|
90
92
|
ndev/win/templates/vhost.conf.tmpl
|
|
@@ -94,4 +96,5 @@ ndev_stack.egg-info/SOURCES.txt
|
|
|
94
96
|
ndev_stack.egg-info/dependency_links.txt
|
|
95
97
|
ndev_stack.egg-info/entry_points.txt
|
|
96
98
|
ndev_stack.egg-info/requires.txt
|
|
97
|
-
ndev_stack.egg-info/top_level.txt
|
|
99
|
+
ndev_stack.egg-info/top_level.txt
|
|
100
|
+
tests/test_shim.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ndev-stack"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -41,5 +41,5 @@ where = ["."]
|
|
|
41
41
|
include = ["ndev*"]
|
|
42
42
|
|
|
43
43
|
[tool.setuptools.package-data]
|
|
44
|
-
"*" = ["*.tmpl", "*.conf", "*.json", "*.ini", "*.php", "*.md"]
|
|
44
|
+
"*" = ["*.tmpl", "*.conf", "*.json", "*.ini", "*.php", "*.md", "*.exe"]
|
|
45
45
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for ndev.win.core.shim (Windows shimgen integration).
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ndev.win.core import shim
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestShim(unittest.TestCase):
|
|
14
|
+
def setUp(self):
|
|
15
|
+
self.test_dir = Path(tempfile.mkdtemp(prefix="ndev_shim_test_"))
|
|
16
|
+
|
|
17
|
+
def tearDown(self):
|
|
18
|
+
shutil.rmtree(self.test_dir, ignore_errors=True)
|
|
19
|
+
|
|
20
|
+
def test_shim_exe_exists(self):
|
|
21
|
+
self.assertTrue(shim.SHIM_EXE.exists(), f"shim.exe not found at {shim.SHIM_EXE}")
|
|
22
|
+
|
|
23
|
+
def test_create_and_read_exe_shim(self):
|
|
24
|
+
# Target dummy executable
|
|
25
|
+
target_path = Path(r"C:\Windows\System32\cmd.exe")
|
|
26
|
+
shim_file = shim.create(
|
|
27
|
+
"dummy_cmd",
|
|
28
|
+
target_path,
|
|
29
|
+
args="/c echo hello",
|
|
30
|
+
cwd=r"C:\Windows",
|
|
31
|
+
path_prepend=r"C:\Windows\System32",
|
|
32
|
+
shim_dir=self.test_dir,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
self.assertEqual(shim_file, self.test_dir / "dummy_cmd.exe")
|
|
36
|
+
self.assertTrue((self.test_dir / "dummy_cmd.exe").exists())
|
|
37
|
+
self.assertTrue((self.test_dir / "dummy_cmd.shim").exists())
|
|
38
|
+
|
|
39
|
+
cfg = shim.read_shim("dummy_cmd", shim_dir=self.test_dir)
|
|
40
|
+
self.assertEqual(cfg.get("path"), str(target_path))
|
|
41
|
+
self.assertEqual(cfg.get("args"), "/c echo hello")
|
|
42
|
+
self.assertEqual(cfg.get("cwd"), r"C:\Windows")
|
|
43
|
+
self.assertEqual(cfg.get("path_prepend"), r"C:\Windows\System32")
|
|
44
|
+
|
|
45
|
+
def test_list_and_remove_shim(self):
|
|
46
|
+
target_path = Path(r"C:\Windows\System32\cmd.exe")
|
|
47
|
+
shim.create("tool_a", target_path, shim_dir=self.test_dir)
|
|
48
|
+
shim.create("tool_b", target_path, shim_dir=self.test_dir)
|
|
49
|
+
|
|
50
|
+
shims = shim.list_shims(shim_dir=self.test_dir)
|
|
51
|
+
self.assertIn("tool_a", shims)
|
|
52
|
+
self.assertIn("tool_b", shims)
|
|
53
|
+
|
|
54
|
+
removed = shim.remove("tool_a", shim_dir=self.test_dir)
|
|
55
|
+
self.assertTrue(removed)
|
|
56
|
+
self.assertFalse((self.test_dir / "tool_a.exe").exists())
|
|
57
|
+
self.assertFalse((self.test_dir / "tool_a.shim").exists())
|
|
58
|
+
|
|
59
|
+
shims_after = shim.list_shims(shim_dir=self.test_dir)
|
|
60
|
+
self.assertNotIn("tool_a", shims_after)
|
|
61
|
+
self.assertIn("tool_b", shims_after)
|
|
62
|
+
|
|
63
|
+
def test_clean_legacy_option(self):
|
|
64
|
+
target_path = Path(r"C:\Windows\System32\cmd.exe")
|
|
65
|
+
# Create mock legacy .bat, .cmd, and .ps1
|
|
66
|
+
(self.test_dir / "preserve_test.bat").write_text("@echo off", encoding="utf-8")
|
|
67
|
+
(self.test_dir / "preserve_test.cmd").write_text("@echo off", encoding="utf-8")
|
|
68
|
+
(self.test_dir / "preserve_test.ps1").write_text("& echo hi", encoding="utf-8")
|
|
69
|
+
|
|
70
|
+
# When clean_legacy is False, legacy files remain
|
|
71
|
+
shim.create("preserve_test", target_path, shim_dir=self.test_dir, clean_legacy=False)
|
|
72
|
+
self.assertTrue((self.test_dir / "preserve_test.exe").exists())
|
|
73
|
+
self.assertTrue((self.test_dir / "preserve_test.bat").exists())
|
|
74
|
+
self.assertTrue((self.test_dir / "preserve_test.cmd").exists())
|
|
75
|
+
self.assertTrue((self.test_dir / "preserve_test.ps1").exists())
|
|
76
|
+
|
|
77
|
+
# When clean_legacy is True, legacy files are unlinked
|
|
78
|
+
shim.create("preserve_test", target_path, shim_dir=self.test_dir, clean_legacy=True)
|
|
79
|
+
self.assertTrue((self.test_dir / "preserve_test.exe").exists())
|
|
80
|
+
self.assertFalse((self.test_dir / "preserve_test.bat").exists())
|
|
81
|
+
self.assertFalse((self.test_dir / "preserve_test.cmd").exists())
|
|
82
|
+
self.assertFalse((self.test_dir / "preserve_test.ps1").exists())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|