ndev-stack 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.
Files changed (79) hide show
  1. ndev/__init__.py +8 -0
  2. ndev/__main__.py +4 -0
  3. ndev/cli.py +24 -0
  4. ndev/common/__init__.py +3 -0
  5. ndev/common/config.py +114 -0
  6. ndev/common/constants.py +51 -0
  7. ndev/common/github.py +13 -0
  8. ndev/common/logger.py +11 -0
  9. ndev/common/manifest.py +41 -0
  10. ndev/common/utils.py +96 -0
  11. ndev/linux/__init__.py +1 -0
  12. ndev/linux/chroot/manager.py +63 -0
  13. ndev/linux/chroot/packages.py +91 -0
  14. ndev/linux/chroot/shell.py +9 -0
  15. ndev/linux/cli.py +235 -0
  16. ndev/linux/commands/available.py +38 -0
  17. ndev/linux/commands/clean.py +25 -0
  18. ndev/linux/commands/ctl.py +192 -0
  19. ndev/linux/commands/current.py +11 -0
  20. ndev/linux/commands/db.py +319 -0
  21. ndev/linux/commands/doctor.py +56 -0
  22. ndev/linux/commands/grok.py +75 -0
  23. ndev/linux/commands/install.py +39 -0
  24. ndev/linux/commands/list.py +47 -0
  25. ndev/linux/commands/logs.py +36 -0
  26. ndev/linux/commands/mailpit.py +82 -0
  27. ndev/linux/commands/reload.py +26 -0
  28. ndev/linux/commands/restart.py +34 -0
  29. ndev/linux/commands/setup.py +113 -0
  30. ndev/linux/commands/start.py +34 -0
  31. ndev/linux/commands/status.py +81 -0
  32. ndev/linux/commands/stop.py +34 -0
  33. ndev/linux/commands/uninstall.py +69 -0
  34. ndev/linux/commands/update.py +57 -0
  35. ndev/linux/commands/upgrade.py +81 -0
  36. ndev/linux/commands/use.py +108 -0
  37. ndev/linux/commands/vhost.py +350 -0
  38. ndev/linux/php/builder.py +183 -0
  39. ndev/linux/php/downloader.py +58 -0
  40. ndev/linux/php/extensions.py +146 -0
  41. ndev/linux/php/installer.py +42 -0
  42. ndev/linux/php/resolver.py +59 -0
  43. ndev/linux/php/templates.py +128 -0
  44. ndev/linux/runtime/fpm.py +117 -0
  45. ndev/linux/runtime/mailpit.py +244 -0
  46. ndev/linux/runtime/pma.py +223 -0
  47. ndev/linux/runtime/process.py +37 -0
  48. ndev/linux/runtime/sockets.py +16 -0
  49. ndev/linux/runtime/upgrade.py +431 -0
  50. ndev/linux/tui.py +1423 -0
  51. ndev/main.py +52 -0
  52. ndev/tui.py +23 -0
  53. ndev/win/__init__.py +1 -0
  54. ndev/win/cli.py +1898 -0
  55. ndev/win/commands/__init__.py +0 -0
  56. ndev/win/core/__init__.py +0 -0
  57. ndev/win/core/db.py +265 -0
  58. ndev/win/core/elevate.py +94 -0
  59. ndev/win/core/ext.py +241 -0
  60. ndev/win/core/fcgi.py +216 -0
  61. ndev/win/core/grok.py +55 -0
  62. ndev/win/core/logs.py +66 -0
  63. ndev/win/core/mailpit.py +236 -0
  64. ndev/win/core/mkcert.py +65 -0
  65. ndev/win/core/paths.py +85 -0
  66. ndev/win/core/php.py +533 -0
  67. ndev/win/core/pma.py +190 -0
  68. ndev/win/core/services.py +349 -0
  69. ndev/win/core/setup.py +361 -0
  70. ndev/win/core/upgrade.py +513 -0
  71. ndev/win/core/vhost.py +289 -0
  72. ndev/win/templates/vhost.conf.tmpl +33 -0
  73. ndev/win/templates/vhost_ssl.conf.tmpl +43 -0
  74. ndev/win/tui.py +1313 -0
  75. ndev_stack-0.1.0.dist-info/METADATA +553 -0
  76. ndev_stack-0.1.0.dist-info/RECORD +79 -0
  77. ndev_stack-0.1.0.dist-info/WHEEL +5 -0
  78. ndev_stack-0.1.0.dist-info/entry_points.txt +4 -0
  79. ndev_stack-0.1.0.dist-info/top_level.txt +1 -0
ndev/win/core/setup.py ADDED
@@ -0,0 +1,361 @@
1
+ """
2
+ `ndev setup` -- download and configure native Windows binaries for Nginx,
3
+ MariaDB, mkcert, ngrok, and Composer.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import shutil
8
+ import stat
9
+ import subprocess
10
+ import urllib.request
11
+ import zipfile
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from . import paths
16
+
17
+ # ---- Defaults ---------------------------------------------------------------
18
+
19
+ DEFAULT_NGINX_VERSION = "1.30.4"
20
+ DEFAULT_MARIADB_VERSION = "11.4.5"
21
+ DEFAULT_MKCERT_VERSION = "1.4.4"
22
+ NGROK_STABLE_URL = "https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-windows-amd64.zip"
23
+ COMPOSER_PHAR_URL = "https://getcomposer.org/composer-stable.phar"
24
+
25
+
26
+ def _download(url: str, dest: Path) -> Path:
27
+ paths.ensure_dirs()
28
+ if dest.exists() and dest.stat().st_size > 0:
29
+ return dest
30
+ tmp = dest.with_suffix(dest.suffix + ".part")
31
+ req = urllib.request.Request(url, headers={"User-Agent": "ndev/0.1.0"})
32
+ try:
33
+ with urllib.request.urlopen(req, timeout=180) as resp, open(tmp, "wb") as f:
34
+ chunk_size = 65536
35
+ while True:
36
+ chunk = resp.read(chunk_size)
37
+ if not chunk:
38
+ break
39
+ f.write(chunk)
40
+ if tmp.exists() and tmp.stat().st_size > 0:
41
+ if dest.exists():
42
+ dest.unlink()
43
+ tmp.rename(dest)
44
+ else:
45
+ raise RuntimeError(f"Download from {url} resulted in empty file.")
46
+ except Exception:
47
+ if tmp.exists():
48
+ tmp.unlink(missing_ok=True)
49
+ raise
50
+ return dest
51
+
52
+
53
+ def _overlay_zip_flatten_single_root(
54
+ zip_path: Path,
55
+ target: Path,
56
+ preserve_dirs: Optional[list[str]] = None,
57
+ preserve_files: Optional[list[str]] = None,
58
+ ) -> None:
59
+ preserve_dirs = preserve_dirs or []
60
+ preserve_files = preserve_files or []
61
+ paths.ensure_dirs()
62
+ target.mkdir(parents=True, exist_ok=True)
63
+
64
+ extract_tmp = paths.DOWNLOADS_DIR / f"_extract_{target.name}"
65
+ if extract_tmp.exists():
66
+ shutil.rmtree(extract_tmp)
67
+
68
+ with zipfile.ZipFile(zip_path) as zf:
69
+ zf.extractall(extract_tmp)
70
+
71
+ inner_dirs = [d for d in extract_tmp.iterdir() if d.is_dir()]
72
+ inner_files = [f for f in extract_tmp.iterdir() if f.is_file()]
73
+ root_src = inner_dirs[0] if (len(inner_dirs) == 1 and not inner_files) else extract_tmp
74
+
75
+ def _copy_tree_recursive(src: Path, dst: Path) -> None:
76
+ dst.mkdir(parents=True, exist_ok=True)
77
+ for item in src.iterdir():
78
+ dest_item = dst / item.name
79
+ if item.is_dir():
80
+ # If directory is in preserve list (e.g. data/ in MariaDB) and already contains user files, DO NOT wipe it!
81
+ if item.name in preserve_dirs and dest_item.exists() and any(dest_item.iterdir()):
82
+ continue
83
+ _copy_tree_recursive(item, dest_item)
84
+ elif item.is_file():
85
+ # If file is in preserve list (e.g. nginx.conf, my.ini) and already exists, DO NOT overwrite it!
86
+ if item.name in preserve_files and dest_item.exists() and dest_item.stat().st_size > 0:
87
+ continue
88
+ shutil.copy2(item, dest_item)
89
+
90
+ _copy_tree_recursive(root_src, target)
91
+ shutil.rmtree(extract_tmp, ignore_errors=True)
92
+
93
+
94
+ # ---- Nginx ------------------------------------------------------------------
95
+
96
+ def install_nginx(version: str = DEFAULT_NGINX_VERSION) -> Path:
97
+ url = f"https://nginx.org/download/nginx-{version}.zip"
98
+ zip_path = _download(url, paths.DOWNLOADS_DIR / f"nginx-{version}.zip")
99
+
100
+ # Non-destructively overlay binaries while preserving conf/ (ndev-vhosts) and logs/
101
+ _overlay_zip_flatten_single_root(
102
+ zip_path,
103
+ paths.NGINX_DIR,
104
+ preserve_dirs=["logs", "temp"],
105
+ preserve_files=["nginx.conf"],
106
+ )
107
+
108
+ paths.NGINX_CONF_D.mkdir(parents=True, exist_ok=True)
109
+ paths.NGINX_LOGS_DIR.mkdir(parents=True, exist_ok=True)
110
+ (paths.NGINX_DIR / "temp").mkdir(parents=True, exist_ok=True)
111
+ _include_vhosts_in_main_conf()
112
+ return paths.NGINX_DIR
113
+
114
+
115
+ def _include_vhosts_in_main_conf() -> None:
116
+ """Add `include ndev-vhosts/*.conf;` inside the http{} block in nginx.conf."""
117
+ # Ensure default fallback vhost exists so Nginx never fails wildcard glob
118
+ default_conf = paths.NGINX_CONF_D / "_default.conf"
119
+ if not default_conf.exists():
120
+ default_conf.write_text(
121
+ 'server {\n'
122
+ ' listen 80 default_server;\n'
123
+ ' server_name _;\n'
124
+ ' return 404 "ndev: No virtual host configured for this domain.\\n";\n'
125
+ '}\n',
126
+ encoding="utf-8"
127
+ )
128
+
129
+ conf = paths.NGINX_DIR / "conf" / "nginx.conf"
130
+ if not conf.exists():
131
+ return
132
+ text = conf.read_text(encoding="utf-8", errors="ignore")
133
+ include_line = " include ndev-vhosts/*.conf;\n"
134
+ if "ndev-vhosts" in text:
135
+ return
136
+
137
+ http_start = text.find("http {")
138
+ if http_start == -1:
139
+ return
140
+ brace_depth = 0
141
+ i = text.find("{", http_start)
142
+ for idx in range(i, len(text)):
143
+ if text[idx] == "{":
144
+ brace_depth += 1
145
+ elif text[idx] == "}":
146
+ brace_depth -= 1
147
+ if brace_depth == 0:
148
+ text = text[:idx] + include_line + text[idx:]
149
+ break
150
+ conf.write_text(text, encoding="utf-8")
151
+
152
+
153
+ # ---- MariaDB ------------------------------------------------------------------
154
+
155
+ def install_mariadb(version: str = DEFAULT_MARIADB_VERSION) -> Path:
156
+ url = f"https://archive.mariadb.org/mariadb-{version}/winx64-packages/mariadb-{version}-winx64.zip"
157
+ zip_path = _download(url, paths.DOWNLOADS_DIR / f"mariadb-{version}-winx64.zip")
158
+
159
+ # Non-destructively overlay binaries while preserving data/ and my.ini
160
+ _overlay_zip_flatten_single_root(
161
+ zip_path,
162
+ paths.MARIADB_DIR,
163
+ preserve_dirs=["data"],
164
+ preserve_files=["my.ini"],
165
+ )
166
+ _init_mariadb_data_dir()
167
+ _create_mariadb_shims()
168
+ return paths.MARIADB_DIR
169
+
170
+
171
+ def _create_mariadb_shims() -> None:
172
+ paths.ensure_dirs()
173
+ mysql_exe = paths.MARIADB_DIR / "bin" / "mysql.exe"
174
+ mysqldump_exe = paths.MARIADB_DIR / "bin" / "mysqldump.exe"
175
+ if mysql_exe.exists():
176
+ (paths.SHIM_DIR / "mysql.bat").write_text(f'@echo off\r\n"{mysql_exe}" %*\r\n', encoding="utf-8")
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")
179
+ if mysqldump_exe.exists():
180
+ (paths.SHIM_DIR / "mysqldump.bat").write_text(f'@echo off\r\n"{mysqldump_exe}" %*\r\n', encoding="utf-8")
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")
183
+
184
+
185
+ def _init_mariadb_data_dir() -> None:
186
+ """Initialize MariaDB data directory with mariadb-install-db.exe and generate isolated my.ini."""
187
+ data_dir = paths.MARIADB_DIR / "data"
188
+ clean_data_dir = str(data_dir.resolve()).replace("\\", "/")
189
+ clean_base_dir = str(paths.MARIADB_DIR.resolve()).replace("\\", "/")
190
+
191
+ # Generate custom my.ini for isolated local execution
192
+ my_ini = paths.MARIADB_DIR / "my.ini"
193
+ if not my_ini.exists():
194
+ my_ini_content = f"""[mysqld]
195
+ basedir = "{clean_base_dir}"
196
+ datadir = "{clean_data_dir}"
197
+ port = 3306
198
+ bind-address = 127.0.0.1
199
+ character-set-server = utf8mb4
200
+ collation-server = utf8mb4_unicode_ci
201
+ max_allowed_packet = 64M
202
+ default-storage-engine = INNODB
203
+ innodb_buffer_pool_size = 128M
204
+
205
+ [client]
206
+ port = 3306
207
+ default-character-set = utf8mb4
208
+
209
+ [mysql]
210
+ default-character-set = utf8mb4
211
+ """
212
+ my_ini.write_text(my_ini_content, encoding="utf-8")
213
+
214
+ if data_dir.exists() and any(data_dir.iterdir()):
215
+ return
216
+
217
+ installer = paths.MARIADB_DIR / "bin" / "mariadb-install-db.exe"
218
+ if not installer.exists():
219
+ installer = paths.MARIADB_DIR / "bin" / "mysql_install_db.exe"
220
+ if not installer.exists():
221
+ return
222
+
223
+ for cmd in [
224
+ [str(installer), f"--datadir={clean_data_dir}", "--service=", "--password=root"],
225
+ [str(installer), f"--datadir={clean_data_dir}", "--password=root"],
226
+ [str(installer), f"--datadir={clean_data_dir}"],
227
+ ]:
228
+ try:
229
+ res = subprocess.run(cmd, cwd=str(paths.MARIADB_DIR), capture_output=True, timeout=60)
230
+ if res.returncode == 0:
231
+ break
232
+ except Exception:
233
+ pass
234
+
235
+
236
+ # ---- mkcert -------------------------------------------------------------------
237
+
238
+ def install_mkcert(version: str = DEFAULT_MKCERT_VERSION) -> Path:
239
+ url = f"https://github.com/FiloSottile/mkcert/releases/download/v{version}/mkcert-v{version}-windows-amd64.exe"
240
+ dest = paths.SHIM_DIR / "mkcert.exe"
241
+ _download(url, dest)
242
+
243
+ cfg = paths.load_config()
244
+ cfg["mkcert_path"] = str(dest)
245
+ paths.save_config(cfg)
246
+
247
+ # Automatically install root CA
248
+ try:
249
+ from . import mkcert
250
+ mkcert.ensure_ca_installed()
251
+ except Exception:
252
+ pass
253
+
254
+ return dest
255
+
256
+
257
+ # ---- ngrok --------------------------------------------------------------------
258
+
259
+ def install_ngrok() -> Path:
260
+ try:
261
+ zip_path = _download(NGROK_STABLE_URL, paths.DOWNLOADS_DIR / "ngrok-stable-windows-amd64.zip")
262
+ with zipfile.ZipFile(zip_path) as zf:
263
+ zf.extract("ngrok.exe", paths.SHIM_DIR)
264
+ dest = paths.SHIM_DIR / "ngrok.exe"
265
+ cfg = paths.load_config()
266
+ cfg["ngrok_path"] = str(dest)
267
+ paths.save_config(cfg)
268
+ return dest
269
+ except Exception as e:
270
+ error_msg = str(e)
271
+ if "virus" in error_msg.lower() or "unwanted" in error_msg.lower() or "225" in error_msg:
272
+ raise RuntimeError(
273
+ "Windows Defender / Antivirus blocked ngrok.exe (Heuristic False Positive). "
274
+ "Reverse-tunnel tools are frequently flagged by antivirus heuristic engines. "
275
+ "You can run `ndev setup --no-ngrok` to skip ngrok installation, or whitelist ~/.ndev/shims/ngrok.exe in Windows Security."
276
+ ) from e
277
+ raise
278
+
279
+
280
+ # ---- Composer -----------------------------------------------------------------
281
+
282
+ def install_composer() -> Path:
283
+ """Download composer.phar and generate composer.bat, composer.cmd, and composer.ps1 shims."""
284
+ phar_dest = paths.SHIM_DIR / "composer.phar"
285
+ _download(COMPOSER_PHAR_URL, phar_dest)
286
+
287
+ clean_phar = str(phar_dest.resolve()).replace("\\", "/")
288
+ (paths.SHIM_DIR / "composer.bat").write_text(f'@echo off\r\nphp "{clean_phar}" %*\r\n', encoding="utf-8")
289
+ (paths.SHIM_DIR / "composer.cmd").write_text(f'@echo off\r\nphp "{clean_phar}" %*\r\n', encoding="utf-8")
290
+ (paths.SHIM_DIR / "composer.ps1").write_text(f'& php "{clean_phar}" @args\r\n', encoding="utf-8")
291
+ return phar_dest
292
+
293
+
294
+ # ---- CA Certificates ---------------------------------------------------------
295
+
296
+ def install_cacert() -> Path:
297
+ """Download Mozilla root CA bundle for PHP cURL and OpenSSL."""
298
+ paths.ensure_dirs()
299
+ if not paths.CACERT_PATH.exists():
300
+ _download("https://curl.se/ca/cacert.pem", paths.CACERT_PATH)
301
+ # Re-apply php.ini updates across all installed PHP runtimes
302
+ from . import php
303
+ for v in php.list_installed():
304
+ php._configure_php_ini(paths.version_dir(v))
305
+ return paths.CACERT_PATH
306
+
307
+
308
+ def create_ndev_shims() -> Path:
309
+ """Create ndev.bat, ndev.cmd, and ndev.ps1 in ~/.ndev/shims/ pointing to active python/ndev."""
310
+ import sys
311
+ paths.ensure_dirs()
312
+ scripts_dir = Path(sys.executable).parent
313
+ ndev_exe = scripts_dir / "ndev.exe"
314
+ if ndev_exe.exists():
315
+ bat_target = f'"{ndev_exe}" %*'
316
+ ps_target = f'& "{ndev_exe}" @args'
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
328
+
329
+
330
+ # ---- Orchestration ----------------------------------------------------------
331
+
332
+ def run_setup(
333
+ nginx: bool = True,
334
+ mariadb: bool = True,
335
+ mkcert: bool = True,
336
+ ngrok: bool = True,
337
+ composer: bool = True,
338
+ cacert: bool = True,
339
+ versions: Optional[dict] = None,
340
+ ) -> dict:
341
+ versions = versions or {}
342
+ results = {}
343
+ if nginx:
344
+ results["nginx"] = install_nginx(versions.get("nginx", DEFAULT_NGINX_VERSION))
345
+ if mariadb:
346
+ results["mariadb"] = install_mariadb(versions.get("mariadb", DEFAULT_MARIADB_VERSION))
347
+ if mkcert:
348
+ results["mkcert"] = install_mkcert(versions.get("mkcert", DEFAULT_MKCERT_VERSION))
349
+ if ngrok:
350
+ try:
351
+ results["ngrok"] = install_ngrok()
352
+ except Exception as e:
353
+ results["ngrok"] = f"Skipped: {e}"
354
+ if composer:
355
+ results["composer"] = install_composer()
356
+ if cacert:
357
+ results["cacert"] = install_cacert()
358
+
359
+ # Always ensure CLI shims
360
+ results["shims"] = create_ndev_shims()
361
+ return results