grimx 0.3.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.
grimx/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
grimx/build.py ADDED
@@ -0,0 +1,142 @@
1
+ """
2
+ grimx.build
3
+ Orchestrate CMake configure, build, test, and run.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ import shutil
10
+ from pathlib import Path
11
+
12
+ import click
13
+
14
+ BUILD_DIR = "build"
15
+
16
+
17
+ def run() -> None:
18
+ """Configure and build the project."""
19
+ _require_tool("cmake")
20
+ _guard_project_root()
21
+ _cmake_configure()
22
+ _cmake_build()
23
+
24
+
25
+ def run_tests() -> None:
26
+ """Run tests via CTest."""
27
+ _require_tool("ctest")
28
+ _guard_project_root()
29
+
30
+ build_path = _build_path()
31
+ if not build_path.exists():
32
+ click.echo("No build directory found — running build first...")
33
+ run()
34
+
35
+ click.echo("Running tests...")
36
+ result = subprocess.run(
37
+ ["ctest", "--output-on-failure", "--test-dir", str(build_path)],
38
+ )
39
+ if result.returncode != 0:
40
+ raise SystemExit(result.returncode)
41
+ click.echo("✓ all tests passed")
42
+
43
+
44
+ def run_app() -> None:
45
+ """Run the compiled application binary."""
46
+ _guard_project_root()
47
+
48
+ build_path = _build_path()
49
+ if not build_path.exists():
50
+ click.echo("error: no build directory found. Run 'grimx build' first.", err=True)
51
+ raise SystemExit(1)
52
+
53
+ project_name = Path.cwd().name
54
+ binary = build_path / project_name
55
+
56
+ # Fallback: look for any executable in build root
57
+ if not binary.exists():
58
+ candidates = [
59
+ p for p in build_path.iterdir()
60
+ if p.is_file() and _is_executable(p)
61
+ ]
62
+ if not candidates:
63
+ click.echo("error: no binary found in build/. Run 'grimx build' first.", err=True)
64
+ raise SystemExit(1)
65
+ binary = candidates[0]
66
+
67
+ click.echo(f"Running {binary.name}...")
68
+ result = subprocess.run([str(binary)])
69
+ raise SystemExit(result.returncode)
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Internal
74
+ # ---------------------------------------------------------------------------
75
+
76
+ def _cmake_configure() -> None:
77
+ project_root = Path.cwd()
78
+ build_path = _build_path()
79
+ build_path.mkdir(exist_ok=True)
80
+
81
+ click.echo("Configuring with CMake...")
82
+ result = subprocess.run(
83
+ [
84
+ "cmake",
85
+ str(project_root), # explicit source dir — never ".."
86
+ f"-B{build_path}", # explicit build dir
87
+ "-DCMAKE_BUILD_TYPE=Debug",
88
+ ],
89
+ cwd=project_root,
90
+ )
91
+ if result.returncode != 0:
92
+ raise SystemExit(result.returncode)
93
+
94
+
95
+ def _cmake_build() -> None:
96
+ build_path = _build_path()
97
+ click.echo("Building...")
98
+ result = subprocess.run(
99
+ ["cmake", "--build", str(build_path), "--parallel"],
100
+ )
101
+ if result.returncode != 0:
102
+ raise SystemExit(result.returncode)
103
+ click.echo("✓ build succeeded")
104
+
105
+
106
+ def _build_path() -> Path:
107
+ return Path.cwd() / BUILD_DIR
108
+
109
+
110
+ def _guard_project_root() -> None:
111
+ """Abort with a clear message if not run from a GRIMX project directory."""
112
+ cwd = Path.cwd()
113
+ has_cmake = (cwd / "CMakeLists.txt").exists()
114
+ has_config = (cwd / "grimx.config").exists()
115
+
116
+ if not has_cmake and not has_config:
117
+ click.echo(
118
+ "error: no CMakeLists.txt or grimx.config found in current directory.\n"
119
+ " Run this command from inside a GRIMX project.",
120
+ err=True,
121
+ )
122
+ raise SystemExit(1)
123
+
124
+ if not has_cmake:
125
+ click.echo(
126
+ "error: CMakeLists.txt not found.\n"
127
+ " Make sure your project was scaffolded correctly.",
128
+ err=True,
129
+ )
130
+ raise SystemExit(1)
131
+
132
+
133
+ def _require_tool(name: str) -> None:
134
+ if not shutil.which(name):
135
+ click.echo(f"error: '{name}' not found in PATH.", err=True)
136
+ click.echo(f" Install it and re-run, or check 'grimx doctor' (coming in v2).")
137
+ raise SystemExit(1)
138
+
139
+
140
+ def _is_executable(path: Path) -> bool:
141
+ import os
142
+ return os.access(path, os.X_OK)
grimx/cli.py ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ grimx.cli
3
+ Entry point for the GRIMX command-line interface
4
+ """
5
+
6
+ import click
7
+ from grimx import __version__
8
+ from grimx import scaffold, install as install_mod, build as build_mod
9
+
10
+ @click.group()
11
+ @click.version_option(__version__, prog_name="grimx")
12
+ def main():
13
+ """GRIMX - GNU Runtime & Installation Manager.
14
+
15
+ Minimal tooling for reproducible C and C++ environments.
16
+ """
17
+
18
+ @main.command()
19
+ @click.argument("name", required=False, default=None)
20
+ @click.option(
21
+ "--type",
22
+ "project_type",
23
+ default=None,
24
+ type=click.Choice(["c", "cpp", "embedded-c", "embedded-cpp"], case_sensitive=False),
25
+ help="Project type (skips prompt if provided).",
26
+ )
27
+ def new(name: str | None, project_type: str | None):
28
+ """Scaffold a new project interactivity, or pass NAME to skip the name prompt."""
29
+ scaffold.create_project(name, project_type)
30
+
31
+
32
+ @main.command("install")
33
+ @click.argument("package", required=False, default=None)
34
+ def install_cmd(package):
35
+ """Install a dependency, or restore all from grimx.lock."""
36
+ install_mod.run(package)
37
+
38
+ @main.command("build")
39
+ def build_cmd():
40
+ """Build the project via CMake."""
41
+ build_mod.run()
42
+
43
+ @main.command("test")
44
+ def test_cmd():
45
+ """Run tests via CTest."""
46
+ build_mod.run_tests()
47
+
48
+ @main.command("run")
49
+ def run_cmd():
50
+ """Run the compiled application."""
51
+ build_mod.run_app()
grimx/config.py ADDED
@@ -0,0 +1,84 @@
1
+ """
2
+ grimx.config
3
+ Read and write grimx.config and grimx.lock using TOML.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import tomlkit
13
+
14
+ CONFIG_FILE = "grimx.config"
15
+ LOCK_FILE = "grimx.lock"
16
+
17
+ DEFAULT_CONFIG: dict[str, Any] = {
18
+ "package_manager": {
19
+ "priority": ["vcpkg", "conan"],
20
+ }
21
+ }
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Config helpers
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def load_config(root: Path | None = None) -> dict[str, Any]:
29
+ """Load grimx.config from root (defaults to cwd)."""
30
+ path = _resolve(root, CONFIG_FILE)
31
+ if not path.exists():
32
+ return dict(DEFAULT_CONFIG)
33
+ return tomlkit.loads(path.read_text())
34
+
35
+
36
+ def write_config(data: dict[str, Any], root: Path | None = None) -> None:
37
+ path = _resolve(root, CONFIG_FILE)
38
+ path.write_text(tomlkit.dumps(data))
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Lock file helpers
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def load_lock(root: Path | None = None) -> dict[str, Any]:
46
+ path = _resolve(root, LOCK_FILE)
47
+ if not path.exists():
48
+ doc = tomlkit.document()
49
+ doc["dependencies"] = tomlkit.table()
50
+ return doc
51
+ return tomlkit.loads(path.read_text())
52
+
53
+
54
+ def write_lock(data: dict[str, Any], root: Path | None = None) -> None:
55
+ path = _resolve(root, LOCK_FILE)
56
+ path.write_text(tomlkit.dumps(data))
57
+
58
+
59
+ def add_dependency(
60
+ name: str,
61
+ manager: str,
62
+ version: str,
63
+ root: Path | None = None,
64
+ ) -> None:
65
+ lock = load_lock(root)
66
+
67
+ if "dependencies" not in lock:
68
+ lock["dependencies"] = tomlkit.table()
69
+
70
+ entry = tomlkit.inline_table()
71
+ entry.append("manager", manager)
72
+ entry.append("version", version)
73
+
74
+ lock["dependencies"][name] = entry
75
+ write_lock(lock, root)
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Internal
80
+ # ---------------------------------------------------------------------------
81
+
82
+ def _resolve(root: Path | None, filename: str) -> Path:
83
+ base = root or Path(os.getcwd())
84
+ return base / filename
grimx/install.py ADDED
@@ -0,0 +1,378 @@
1
+ """
2
+ grimx.install
3
+ Delegate package installation to vcpkg or Conan with fallback logic.
4
+ Offers to auto-install missing package managers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import platform
12
+ import subprocess
13
+ import shutil
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ import click
18
+
19
+ from grimx.config import load_config, load_lock, add_dependency
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Public API
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def run(package: str | None) -> None:
27
+ if package:
28
+ _install_package(package)
29
+ else:
30
+ _restore_from_lock()
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Install / restore
35
+ # ---------------------------------------------------------------------------
36
+
37
+ def _install_package(package: str) -> None:
38
+ cfg = load_config()
39
+ priority: list[str] = cfg.get("package_manager", {}).get("priority", [])
40
+
41
+ if not priority:
42
+ click.echo(
43
+ "error: no package managers configured.\n"
44
+ " Edit grimx.config and set priority = [\"vcpkg\"] or [\"conan\"].",
45
+ err=True,
46
+ )
47
+ raise SystemExit(1)
48
+
49
+ for manager in priority:
50
+ if not _is_manager_available(manager):
51
+ if not _prompt_and_install_manager(manager):
52
+ continue
53
+
54
+ click.echo(f" trying {manager}...")
55
+ ok, version = _try_install(manager, package)
56
+ if ok:
57
+ add_dependency(package, manager, version)
58
+ click.echo(f" ✓ installed {package}=={version} via {manager}")
59
+ click.echo(f" ✓ grimx.lock updated")
60
+ if manager == "vcpkg":
61
+ _sync_vcpkg_manifest()
62
+ click.echo(f" ✓ vcpkg.json updated")
63
+ return
64
+
65
+ click.echo(
66
+ f"\nerror: could not install '{package}' with any configured manager "
67
+ f"({', '.join(priority)}).",
68
+ err=True,
69
+ )
70
+ raise SystemExit(1)
71
+
72
+
73
+ def _restore_from_lock() -> None:
74
+ lock = load_lock()
75
+ deps: dict = lock.get("dependencies", {})
76
+
77
+ if not deps:
78
+ click.echo("grimx.lock is empty — nothing to install.")
79
+ return
80
+
81
+ click.echo(f"Restoring {len(deps)} dependencies from grimx.lock...")
82
+
83
+ # Group by manager
84
+ vcpkg_deps = {k: v for k, v in deps.items() if v["manager"] == "vcpkg"}
85
+ conan_deps = {k: v for k, v in deps.items() if v["manager"] == "conan"}
86
+
87
+ failed = []
88
+
89
+ if vcpkg_deps:
90
+ if not _is_manager_available("vcpkg"):
91
+ if not _prompt_and_install_manager("vcpkg"):
92
+ failed.extend(vcpkg_deps.keys())
93
+ vcpkg_deps = {}
94
+
95
+ if vcpkg_deps:
96
+ if _restore_vcpkg(vcpkg_deps):
97
+ for name in vcpkg_deps:
98
+ click.echo(f" ✓ {name}")
99
+ else:
100
+ failed.extend(vcpkg_deps.keys())
101
+
102
+ for name, meta in conan_deps.items():
103
+ if not _is_manager_available("conan"):
104
+ if not _prompt_and_install_manager("conan"):
105
+ click.echo(f" ✗ {name} — skipped (conan unavailable)", err=True)
106
+ failed.append(name)
107
+ continue
108
+
109
+ click.echo(f" installing {name}=={meta['version']} via conan...")
110
+ ok, _ = _try_install("conan", name)
111
+ if ok:
112
+ click.echo(f" ✓ {name}")
113
+ else:
114
+ click.echo(f" ✗ {name} — install failed", err=True)
115
+ failed.append(name)
116
+
117
+ if failed:
118
+ click.echo(f"\n{len(failed)} package(s) failed: {', '.join(failed)}", err=True)
119
+ raise SystemExit(1)
120
+
121
+ click.echo(f"\n✓ all dependencies restored")
122
+
123
+
124
+ def _restore_vcpkg(deps: dict) -> bool:
125
+ """Restore vcpkg deps via vcpkg.json manifest for version pinning."""
126
+ if not _sync_vcpkg_manifest():
127
+ return False
128
+
129
+ click.echo(f" generated vcpkg.json with {len(deps)} package(s)")
130
+
131
+ result = subprocess.run([_vcpkg_bin(), "install"], text=True)
132
+
133
+ if result.returncode != 0:
134
+ click.echo(" error: vcpkg install failed.", err=True)
135
+ return False
136
+
137
+ click.echo(" ✓ all vcpkg dependencies restored")
138
+ return True
139
+
140
+
141
+ def _sync_vcpkg_manifest() -> bool:
142
+ """Generate vcpkg.json from current grimx.lock state."""
143
+ lock = load_lock()
144
+ deps = {k: v for k, v in lock.get("dependencies", {}).items() if v["manager"] == "vcpkg"}
145
+
146
+ if not deps:
147
+ return True
148
+
149
+ baseline_result = subprocess.run(
150
+ ["git", "-C", str(Path.home() / ".vcpkg"), "rev-parse", "HEAD"],
151
+ capture_output=True, text=True,
152
+ )
153
+ if baseline_result.returncode != 0:
154
+ click.echo(" error: could not get vcpkg baseline.", err=True)
155
+ return False
156
+
157
+ baseline = baseline_result.stdout.strip()
158
+
159
+ manifest = {
160
+ "name": "grimx-project",
161
+ "version": "0.1.0",
162
+ "builtin-baseline": baseline,
163
+ "dependencies": [
164
+ {"name": name, "version>=": meta["version"]}
165
+ if meta["version"] != "unknown"
166
+ else name
167
+ for name, meta in deps.items()
168
+ ]
169
+ }
170
+
171
+ manifest_path = Path.cwd() / "vcpkg.json"
172
+ manifest_path.write_text(json.dumps(manifest, indent=2))
173
+ return True
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Manager availability
178
+ # ---------------------------------------------------------------------------
179
+
180
+ def _is_manager_available(manager: str) -> bool:
181
+ """Check if a package manager is available via PATH or known install location."""
182
+ if manager == "vcpkg":
183
+ return bool(shutil.which("vcpkg")) or (Path.home() / ".vcpkg" / "vcpkg").exists()
184
+ return bool(shutil.which(manager))
185
+
186
+
187
+ def _vcpkg_bin() -> str:
188
+ """Return vcpkg binary — from PATH or known install location."""
189
+ return shutil.which("vcpkg") or str(Path.home() / ".vcpkg" / "vcpkg")
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # Auto-install managers
194
+ # ---------------------------------------------------------------------------
195
+
196
+ def _prompt_and_install_manager(manager: str) -> bool:
197
+ """Offer to install a missing package manager. Returns True if now available."""
198
+ hints = {
199
+ "vcpkg": "https://vcpkg.io/en/getting-started",
200
+ "conan": "pip install conan",
201
+ }
202
+ installers = {
203
+ "vcpkg": _auto_install_vcpkg,
204
+ "conan": _auto_install_conan,
205
+ }
206
+
207
+ click.echo(f"\n '{manager}' is not installed.")
208
+
209
+ if manager not in installers:
210
+ click.echo(f" No auto-install available for '{manager}'.", err=True)
211
+ return False
212
+
213
+ click.echo(f" Install hint: {hints[manager]}")
214
+ if not click.confirm(f" Install {manager} now?", default=True):
215
+ click.echo(f" Skipping {manager}.")
216
+ return False
217
+
218
+ success = installers[manager]()
219
+
220
+ if success and _is_manager_available(manager):
221
+ return True
222
+
223
+ click.echo(
224
+ f"\n '{manager}' still not found after install.\n"
225
+ f" You may need to open a new terminal or add it to PATH manually.",
226
+ err=True,
227
+ )
228
+ return False
229
+
230
+
231
+ def _auto_install_vcpkg() -> bool:
232
+ """Clone and bootstrap vcpkg into ~/.vcpkg and add to session PATH."""
233
+ vcpkg_dir = Path.home() / ".vcpkg"
234
+
235
+ # Already fully installed
236
+ if vcpkg_dir.exists() and (vcpkg_dir / "vcpkg").exists():
237
+ click.echo(" vcpkg binary found, skipping clone and bootstrap.")
238
+ os.environ["PATH"] = str(vcpkg_dir) + os.pathsep + os.environ.get("PATH", "")
239
+ return True
240
+
241
+ # Stale/incomplete clone
242
+ if vcpkg_dir.exists() and not (vcpkg_dir / "vcpkg").exists():
243
+ click.echo(" ~/.vcpkg exists but vcpkg binary is missing. Removing and re-cloning...")
244
+ shutil.rmtree(vcpkg_dir)
245
+
246
+ # Fresh clone
247
+ if not vcpkg_dir.exists():
248
+ click.echo(" Cloning vcpkg into ~/.vcpkg ...")
249
+ result = subprocess.run(
250
+ ["git", "clone", "https://github.com/microsoft/vcpkg.git", str(vcpkg_dir)],
251
+ )
252
+ if result.returncode != 0:
253
+ click.echo(" error: git clone failed.", err=True)
254
+ return False
255
+
256
+ click.echo(" Bootstrapping vcpkg...")
257
+ if platform.system() == "Windows":
258
+ bootstrap = vcpkg_dir / "bootstrap-vcpkg.bat"
259
+ cmd = [str(bootstrap), "-disableMetrics"]
260
+ else:
261
+ bootstrap = vcpkg_dir / "bootstrap-vcpkg.sh"
262
+ cmd = ["bash", str(bootstrap), "-disableMetrics"]
263
+
264
+ if not bootstrap.exists():
265
+ click.echo(f" error: bootstrap script not found at {bootstrap}", err=True)
266
+ click.echo(f" The clone may have failed silently. Try: rm -rf ~/.vcpkg and retry.", err=True)
267
+ return False
268
+
269
+ if platform.system() != "Windows":
270
+ bootstrap.chmod(bootstrap.stat().st_mode | 0o111)
271
+
272
+ result = subprocess.run(cmd)
273
+ if result.returncode != 0:
274
+ click.echo(" error: bootstrap failed.", err=True)
275
+ return False
276
+
277
+ _persist_to_path(vcpkg_dir)
278
+ os.environ["PATH"] = str(vcpkg_dir) + os.pathsep + os.environ.get("PATH", "")
279
+
280
+ click.echo(f" ✓ vcpkg installed at {vcpkg_dir}")
281
+ return True
282
+
283
+
284
+ def _persist_to_path(directory: Path) -> None:
285
+ """Append directory to PATH in the user's shell profile."""
286
+ export_line = f'export PATH="{directory}:$PATH"'
287
+
288
+ shell = os.environ.get("SHELL", "")
289
+ if "zsh" in shell:
290
+ profile = Path.home() / ".zshrc"
291
+ else:
292
+ profile = Path.home() / ".bashrc"
293
+
294
+ if profile.exists() and export_line in profile.read_text():
295
+ return
296
+
297
+ with profile.open("a") as f:
298
+ f.write(f"\n# Added by grimx\n{export_line}\n")
299
+
300
+ click.echo(f" ✓ added to {profile} — run 'source {profile}' or open a new terminal")
301
+
302
+
303
+ def _auto_install_conan() -> bool:
304
+ """Install conan via pip, falling back to pipx if pip is blocked."""
305
+ click.echo(" Installing conan via pip...")
306
+ result = subprocess.run(
307
+ [sys.executable, "-m", "pip", "install", "conan", "--quiet"],
308
+ )
309
+ if result.returncode == 0:
310
+ click.echo(" ✓ conan installed")
311
+ return True
312
+
313
+ if shutil.which("pipx"):
314
+ click.echo(" pip blocked — trying pipx...")
315
+ result = subprocess.run(["pipx", "install", "conan"])
316
+ if result.returncode == 0:
317
+ local_bin = str(Path.home() / ".local" / "bin")
318
+ os.environ["PATH"] = local_bin + os.pathsep + os.environ.get("PATH", "")
319
+ click.echo(" ✓ conan installed via pipx")
320
+ return True
321
+
322
+ click.echo(" error: could not install conan.", err=True)
323
+ click.echo(" Try manually: pip install conan or pipx install conan", err=True)
324
+ return False
325
+
326
+
327
+ # ---------------------------------------------------------------------------
328
+ # Package manager wrappers
329
+ # ---------------------------------------------------------------------------
330
+
331
+ def _try_install(manager: str, package: str) -> tuple[bool, str]:
332
+ if manager == "vcpkg":
333
+ return _vcpkg_install(package)
334
+ if manager == "conan":
335
+ return _conan_install(package)
336
+ return False, ""
337
+
338
+
339
+ def _vcpkg_install(package: str) -> tuple[bool, str]:
340
+ result = subprocess.run(
341
+ [_vcpkg_bin(), "install", package],
342
+ capture_output=True, text=True,
343
+ )
344
+ if result.stdout:
345
+ click.echo(result.stdout, nl=False)
346
+ if result.stderr:
347
+ click.echo(result.stderr, nl=False)
348
+
349
+ if result.returncode == 0:
350
+ combined = result.stdout + result.stderr
351
+ return True, _parse_vcpkg_version(combined, package)
352
+ return False, ""
353
+
354
+
355
+ def _conan_install(package: str) -> tuple[bool, str]:
356
+ result = subprocess.run(
357
+ ["conan", "install", "--requires", package, "--build=missing"],
358
+ capture_output=True, text=True,
359
+ )
360
+ if result.returncode == 0:
361
+ return True, _parse_conan_version(result.stdout, package)
362
+ return False, ""
363
+
364
+
365
+ def _parse_vcpkg_version(output: str, package: str) -> str:
366
+ """Parse version from vcpkg output — handles 'fmt:x64-linux@12.1.0' format."""
367
+ for line in output.splitlines():
368
+ if package.lower() in line.lower() and "@" in line:
369
+ return line.strip().split("@")[-1]
370
+ return "unknown"
371
+
372
+
373
+ def _parse_conan_version(output: str, package: str) -> str:
374
+ base = package.split("/")[0]
375
+ for line in output.splitlines():
376
+ if "/" in line and base.lower() in line.lower():
377
+ return line.strip().split("/")[-1].split("@")[0]
378
+ return "unknown"
grimx/scaffold.py ADDED
@@ -0,0 +1,238 @@
1
+ """
2
+ grimx.scaffold
3
+ Interactive project creation — create-next-app style.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import shutil
9
+ from importlib import resources
10
+ from pathlib import Path
11
+
12
+ import click
13
+
14
+ from grimx.config import write_config, write_lock, DEFAULT_CONFIG
15
+
16
+ TEMPLATE_MAP = {
17
+ "c": "c",
18
+ "cpp": "cpp",
19
+ "embedded-c": "embedded_c",
20
+ "embedded-cpp": "embedded_cpp",
21
+ }
22
+
23
+ PROJECT_TYPES = ["cpp", "c", "embedded-cpp", "embedded-c"]
24
+ CPP_STANDARDS = ["17", "20", "14"]
25
+ C_STANDARDS = ["11", "17", "99"]
26
+ MANAGERS = ["vcpkg", "conan", "both", "none"]
27
+
28
+
29
+ def create_project(name: str | None, project_type: str | None) -> None:
30
+ click.echo("")
31
+
32
+ # ── Project name ──────────────────────────────────────────────────────
33
+ if not name:
34
+ name = click.prompt(" Project name", default="my_project")
35
+
36
+ dest = Path.cwd() / name
37
+ if dest.exists():
38
+ click.echo(f"\nerror: directory '{name}' already exists.", err=True)
39
+ raise SystemExit(1)
40
+
41
+ # ── Project type ──────────────────────────────────────────────────────
42
+ if not project_type:
43
+ click.echo("")
44
+ click.echo(" What type of project?")
45
+ for i, t in enumerate(PROJECT_TYPES, 1):
46
+ marker = " (default)" if t == "cpp" else ""
47
+ click.echo(f" {i}. {t}{marker}")
48
+ choice = click.prompt(" Choice", default="1", show_default=False)
49
+ try:
50
+ project_type = PROJECT_TYPES[int(choice) - 1]
51
+ except (ValueError, IndexError):
52
+ project_type = "cpp"
53
+
54
+ # ── Language standard ─────────────────────────────────────────────────
55
+ is_cpp = "cpp" in project_type
56
+ if is_cpp:
57
+ standards = CPP_STANDARDS
58
+ lang = "C++"
59
+ default_std = "17"
60
+ else:
61
+ standards = C_STANDARDS
62
+ lang = "C"
63
+ default_std = "11"
64
+
65
+ click.echo("")
66
+ click.echo(f" {lang} standard?")
67
+ for i, s in enumerate(standards, 1):
68
+ marker = " (default)" if s == default_std else ""
69
+ click.echo(f" {i}. {lang}{s}{marker}")
70
+ std_choice = click.prompt(" Choice", default="1", show_default=False)
71
+ try:
72
+ std = standards[int(std_choice) - 1]
73
+ except (ValueError, IndexError):
74
+ std = default_std
75
+
76
+ # ── Package manager ───────────────────────────────────────────────────
77
+ click.echo("")
78
+ click.echo(" Package manager?")
79
+ for i, m in enumerate(MANAGERS, 1):
80
+ marker = " (default)" if m == "vcpkg" else ""
81
+ click.echo(f" {i}. {m}{marker}")
82
+ mgr_choice = click.prompt(" Choice", default="1", show_default=False)
83
+ try:
84
+ mgr = MANAGERS[int(mgr_choice) - 1]
85
+ except (ValueError, IndexError):
86
+ mgr = "vcpkg"
87
+
88
+ if mgr == "both":
89
+ priority = ["vcpkg", "conan"]
90
+ elif mgr == "none":
91
+ priority = []
92
+ else:
93
+ priority = [mgr]
94
+
95
+ # ── Summary ───────────────────────────────────────────────────────────
96
+ click.echo("")
97
+ click.echo(f" Creating project '{name}'")
98
+ click.echo(f" type : {project_type}")
99
+ click.echo(f" standard : {lang}{std}")
100
+ click.echo(f" managers : {', '.join(priority) if priority else 'none'}")
101
+ click.echo("")
102
+
103
+ # ── Scaffold ──────────────────────────────────────────────────────────
104
+ template_src = _get_template_path(TEMPLATE_MAP[project_type])
105
+ shutil.copytree(template_src, dest)
106
+
107
+ for gitkeep in dest.rglob(".gitkeep"):
108
+ gitkeep.unlink()
109
+
110
+ _patch_cmakelists(dest, name, project_type, std)
111
+ _write_readme(dest, name, project_type, std)
112
+ _write_gitignore(dest)
113
+ _write_clang_format(dest)
114
+
115
+ # Ensure all expected directories exist
116
+ for d in ["include", "docs", "cmake"]:
117
+ (dest / d).mkdir(exist_ok=True)
118
+
119
+ write_config({"package_manager": {"priority": priority}}, root=dest)
120
+ write_lock({"dependencies": {}}, root=dest)
121
+
122
+ click.echo(f" ✓ {dest}")
123
+ click.echo("")
124
+ click.echo(" Next steps:")
125
+ click.echo(f" cd {name}")
126
+ if priority:
127
+ click.echo(f" grimx install <package>")
128
+ click.echo(f" grimx build")
129
+ click.echo(f" grimx run")
130
+ click.echo("")
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Patching and file generation
135
+ # ---------------------------------------------------------------------------
136
+
137
+ def _patch_cmakelists(dest: Path, name: str, project_type: str, std: str) -> None:
138
+ cmake = dest / "CMakeLists.txt"
139
+ if not cmake.exists():
140
+ return
141
+ text = cmake.read_text()
142
+ is_cpp = "cpp" in project_type
143
+ text = text.replace("project(PROJECT_NAME", f"project({name}")
144
+ if is_cpp:
145
+ text = text.replace("set(CMAKE_CXX_STANDARD 17)", f"set(CMAKE_CXX_STANDARD {std})")
146
+ else:
147
+ text = text.replace("set(CMAKE_C_STANDARD 11)", f"set(CMAKE_C_STANDARD {std})")
148
+ cmake.write_text(text)
149
+
150
+
151
+ def _write_readme(dest: Path, name: str, project_type: str, std: str) -> None:
152
+ is_cpp = "cpp" in project_type
153
+ lang = "C++" if is_cpp else "C"
154
+ content = f"""# {name}
155
+
156
+ A {lang}{std} project.
157
+
158
+ ## Build
159
+
160
+ ```bash
161
+ grimx build
162
+ grimx test
163
+ grimx run
164
+ ```
165
+
166
+ ## Dependencies
167
+
168
+ Install a dependency:
169
+
170
+ ```bash
171
+ grimx install <package>
172
+ ```
173
+
174
+ Restore from lock file:
175
+
176
+ ```bash
177
+ grimx install
178
+ ```
179
+
180
+ ## Project Structure
181
+
182
+ ```
183
+ {name}/
184
+ src/ source files
185
+ include/ project headers
186
+ tests/ unit tests
187
+ docs/ documentation
188
+ cmake/ cmake modules
189
+ CMakeLists.txt
190
+ grimx.config
191
+ grimx.lock
192
+ ```
193
+ """
194
+ (dest / "README.md").write_text(content)
195
+
196
+
197
+ def _write_gitignore(dest: Path) -> None:
198
+ (dest / ".gitignore").write_text(
199
+ "# Build output\n"
200
+ "build/\n"
201
+ "out/\n\n"
202
+ "# Dependencies\n"
203
+ "vcpkg_installed/\n"
204
+ ".conan/\n\n"
205
+ "# Editor\n"
206
+ ".vscode/\n"
207
+ ".idea/\n"
208
+ "*.swp\n"
209
+ "*.swo\n\n"
210
+ "# OS\n"
211
+ ".DS_Store\n"
212
+ "Thumbs.db\n\n"
213
+ "# GRIMX\n"
214
+ "grimx.lock\n\n"
215
+ )
216
+
217
+
218
+ def _write_clang_format(dest: Path) -> None:
219
+ (dest / ".clang-format").write_text(
220
+ "---\n"
221
+ "BasedOnStyle: LLVM\n"
222
+ "IndentWidth: 4\n"
223
+ "ColumnLimit: 100\n"
224
+ "AllowShortFunctionsOnASingleLine: None\n"
225
+ "AllowShortIfStatementsOnASingleLine: Never\n"
226
+ "BreakBeforeBraces: Attach\n"
227
+ "---\n"
228
+ )
229
+
230
+
231
+ # ---------------------------------------------------------------------------
232
+ # Template resolution
233
+ # ---------------------------------------------------------------------------
234
+
235
+ def _get_template_path(template_key: str) -> Path:
236
+ pkg = resources.files("grimx") / "templates" / template_key
237
+ with resources.as_file(pkg) as path:
238
+ return Path(str(path)).resolve()
@@ -0,0 +1,12 @@
1
+ cmake_minimum_required(VERSION 3.20)
2
+ project(PROJECT_NAME C)
3
+
4
+ set(CMAKE_C_STANDARD 11)
5
+ set(CMAKE_C_STANDARD_REQUIRED ON)
6
+
7
+ include_directories(include)
8
+
9
+ add_executable(${PROJECT_NAME} src/main.c)
10
+
11
+ enable_testing()
12
+ add_subdirectory(tests)
File without changes
@@ -0,0 +1,4 @@
1
+ # Add test executables here.
2
+ # Example:
3
+ # add_executable(test_example test_example.c)
4
+ # add_test(NAME test_example COMMAND test_example)
@@ -0,0 +1,12 @@
1
+ cmake_minimum_required(VERSION 3.20)
2
+ project(PROJECT_NAME CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+
7
+ include_directories(include)
8
+
9
+ add_executable(${PROJECT_NAME} src/main.cpp)
10
+
11
+ enable_testing()
12
+ add_subdirectory(tests)
@@ -0,0 +1,7 @@
1
+ #include <iostream>
2
+
3
+ int main()
4
+ {
5
+ std::cout << "Hello from GRIMX!" << std::endl;
6
+ return 0;
7
+ }
@@ -0,0 +1,4 @@
1
+ # Add test executables here.
2
+ # Example:
3
+ # add_executable(test_example test_example.cpp)
4
+ # add_test(NAME test_example COMMAND test_example)
@@ -0,0 +1,15 @@
1
+ cmake_minimum_required(VERSION 3.20)
2
+ project(PROJECT_NAME C)
3
+
4
+ set(CMAKE_C_STANDARD 11)
5
+ set(CMAKE_C_STANDARD_REQUIRED ON)
6
+
7
+ # Toolchain file should be passed via -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake
8
+ # or configured by GRIMX for the target architecture.
9
+
10
+ include_directories(include)
11
+
12
+ add_executable(${PROJECT_NAME} src/main.c)
13
+
14
+ # enable_testing()
15
+ # add_subdirectory(tests)
@@ -0,0 +1,8 @@
1
+ # Toolchain file stub — configure for your target.
2
+ # Example for ARM bare-metal:
3
+ #
4
+ # set(CMAKE_SYSTEM_NAME Generic)
5
+ # set(CMAKE_SYSTEM_PROCESSOR arm)
6
+ # set(CMAKE_C_COMPILER arm-none-eabi-gcc)
7
+ # set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
8
+ # set(CMAKE_EXE_LINKER_FLAGS "--specs=nosys.specs" CACHE INTERNAL "")
@@ -0,0 +1,10 @@
1
+ /* Embedded C entry point
2
+ * Replace with your target's startup/main conventions as needed.
3
+ */
4
+
5
+ void main(void) {
6
+ /* Application entry */
7
+ while(1) {
8
+ /* main loop */
9
+ }
10
+ }
@@ -0,0 +1,17 @@
1
+ cmake_minimum_required(VERSION 3.20)
2
+ project(PROJECT_NAME CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+
7
+ # Toolchain file should be passed via -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake
8
+ # or configured by GRIMX for the target architecture.
9
+
10
+ include_directories(include)
11
+
12
+ add_executable(${PROJECT_NAME} src/main.cpp)
13
+
14
+ # Embedded projects commonly disable the standard host test runner.
15
+ # Uncomment and adapt for on-target or emulated testing:
16
+ # enable_testing()
17
+ # add_subdirectory(tests)
@@ -0,0 +1,9 @@
1
+ # Toolchain file stub — configure for your target.
2
+ # Example for ARM bare-metal:
3
+ #
4
+ # set(CMAKE_SYSTEM_NAME Generic)
5
+ # set(CMAKE_SYSTEM_PROCESSOR arm)
6
+ # set(CMAKE_C_COMPILER arm-none-eabi-gcc)
7
+ # set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
8
+ # set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
9
+ # set(CMAKE_EXE_LINKER_FLAGS "--specs=nosys.specs" CACHE INTERNAL "")
@@ -0,0 +1,9 @@
1
+ // Embedded C++ entry point
2
+ // Replace with your target's startup/main conventions needed.
3
+
4
+ extern "C" void main() {
5
+ // Application entry
6
+ while(true) {
7
+ // main loop
8
+ }
9
+ }
File without changes
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: grimx
3
+ Version: 0.3.0
4
+ Summary: GCC Runtime & Installation Manager, Cross-platform — minimal tooling for C and C++ projects
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 GRIMX LABS
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: click>=8.1
31
+ Requires-Dist: tomlkit>=0.12
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # GRIMX
37
+
38
+ **GCC Runtime & Installation Manager - Cross Platform**
39
+
40
+ A minimal developer tool for reproducible C and C++ environments.
41
+
42
+ ---
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install grimx
48
+ ```
49
+
50
+ That's it. The `grimx` command is now available globally.
51
+
52
+ ---
53
+
54
+ ## Quick Start
55
+
56
+ ```bash
57
+ grimx new hello_world
58
+ cd hello_world
59
+ grimx install fmt
60
+ grimx build
61
+ grimx test
62
+ grimx run
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Commands
68
+
69
+ | Command | Description |
70
+ |---|---|
71
+ | `grimx new <name>` | Scaffold a new project |
72
+ | `grimx new <name> --type c` | Scaffold a C project (default: cpp) |
73
+ | `grimx install <pkg>` | Install a dependency |
74
+ | `grimx install` | Restore all dependencies from lock file |
75
+ | `grimx build` | Build the project via CMake |
76
+ | `grimx test` | Run tests via CTest |
77
+ | `grimx run` | Run the compiled application |
78
+
79
+ ---
80
+
81
+ ## Project Structure
82
+
83
+ ```
84
+ my_project/
85
+ src/ source files
86
+ include/ project headers
87
+ tests/ unit tests
88
+ cmake/ optional cmake modules
89
+ CMakeLists.txt
90
+ grimx.config
91
+ grimx.lock
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Project Types
97
+
98
+ ```bash
99
+ grimx new my_app --type c # C application
100
+ grimx new my_app --type cpp # C++ application (default)
101
+ grimx new my_fw --type embedded-c # Embedded C
102
+ grimx new my_fw --type embedded-cpp # Embedded C++
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Contributing
108
+
109
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
110
+
111
+ ---
112
+
113
+ ## License
114
+
115
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,25 @@
1
+ grimx/__init__.py,sha256=Pru0BlFBASFCFo7McHdohtKkUtgMPDwbGfyUZlE2_Vw,21
2
+ grimx/build.py,sha256=Mow7T7rRhqqHSaQZpNDAuOHxN_7ws53CdnKbKPDIsKk,3892
3
+ grimx/cli.py,sha256=hUqKPQIlAGlu1HYwEs3fVekiXJBvCXXuuO9UUatiThg,1359
4
+ grimx/config.py,sha256=6iuTRjDWDmiRjNkUl3uriibDVFiBG7tvLzaqQN_WdXs,2230
5
+ grimx/install.py,sha256=WOBOAtBUASILljjoiStJbjimgHz1BeAy7oca2bfuue0,12366
6
+ grimx/scaffold.py,sha256=vvf1x8t8XGFpuWP6M-ur9o-oqLGkYTKrtgcu86aUpHw,7356
7
+ grimx/templates/c/CMakeLists.txt,sha256=VzK4lsdsznER8lnBJYIM0DHtBjKUX7lwFWLAY4Ov0bc,237
8
+ grimx/templates/c/src/main.c,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ grimx/templates/c/tests/CMakeLists.txt,sha256=RxBJVDnT9PQ23ttOOi20UBgIavepsarLgyHneo9QZO8,141
10
+ grimx/templates/cpp/CMakeLists.txt,sha256=-ir6f1X6xgnezIj7j0wUEgdX3JrklYoKb58i7aaNsTQ,245
11
+ grimx/templates/cpp/src/main.cpp,sha256=ILaZwjfyq8U5ehM0BQ95adimjtEMrW7JSEUzFdGTJsQ,100
12
+ grimx/templates/cpp/tests/CMakeLists.txt,sha256=AT2xV7W06bjnvp_87XeZMSSk6c08bHSbWE1wnOH9Zso,143
13
+ grimx/templates/embedded_c/CMakeLists.txt,sha256=RjObXGkWA4VURDgggo8opFRZF4BFB2YHLUXRAFICbyE,379
14
+ grimx/templates/embedded_c/cmake/toolchain.cmake,sha256=rKDRleSjz-DJU9t9j75jRyJO_8g2h7z0drUDFdy0n-c,310
15
+ grimx/templates/embedded_c/src/main.c,sha256=wCvnEv7JrqNLvKdU49VKuggF2N5A091lCGH_gizRZz8,188
16
+ grimx/templates/embedded_cpp/CMakeLists.txt,sha256=1C3hPt6awDY8CZgmTKw1JKsew6s6PKr3OSaprBXrIWg,512
17
+ grimx/templates/embedded_cpp/cmake/toolchain_cmake,sha256=rC1wZ4dP5r-5bxqr3G_S8SNe1KkASL5DfffbSp7OwNY,354
18
+ grimx/templates/embedded_cpp/src/main.cpp,sha256=cBS3K1pjglwF_7fMrc6sR32rbFwdCFUP-E7iqQ0xML0,191
19
+ "grimx/templates/embedded_cpp/tests/,gitkeep",sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ grimx-0.3.0.dist-info/licenses/LICENSE,sha256=nh_kJ1XfldFWSoUZ2MH8ks8-UiEGLC7ralU72qtsDn4,1067
21
+ grimx-0.3.0.dist-info/METADATA,sha256=_BUVarlQIAUTGXfxCz_S_Myr6WKykVfjP9o7eyn-S4U,3005
22
+ grimx-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
+ grimx-0.3.0.dist-info/entry_points.txt,sha256=Ql4Ez2SAzxuRDBcEx7sdSeZR-sVOmZgFUFZxDD45TS4,41
24
+ grimx-0.3.0.dist-info/top_level.txt,sha256=l4Om6ZDVTXV6MDxvc-Qo0s045jH-1OOVtv67XVPR_0U,6
25
+ grimx-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ grimx = grimx.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GRIMX LABS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ grimx