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 +1 -0
- grimx/build.py +142 -0
- grimx/cli.py +51 -0
- grimx/config.py +84 -0
- grimx/install.py +378 -0
- grimx/scaffold.py +238 -0
- grimx/templates/c/CMakeLists.txt +12 -0
- grimx/templates/c/src/main.c +0 -0
- grimx/templates/c/tests/CMakeLists.txt +4 -0
- grimx/templates/cpp/CMakeLists.txt +12 -0
- grimx/templates/cpp/src/main.cpp +7 -0
- grimx/templates/cpp/tests/CMakeLists.txt +4 -0
- grimx/templates/embedded_c/CMakeLists.txt +15 -0
- grimx/templates/embedded_c/cmake/toolchain.cmake +8 -0
- grimx/templates/embedded_c/src/main.c +10 -0
- grimx/templates/embedded_cpp/CMakeLists.txt +17 -0
- grimx/templates/embedded_cpp/cmake/toolchain_cmake +9 -0
- grimx/templates/embedded_cpp/src/main.cpp +9 -0
- grimx/templates/embedded_cpp/tests/,gitkeep +0 -0
- grimx-0.3.0.dist-info/METADATA +115 -0
- grimx-0.3.0.dist-info/RECORD +25 -0
- grimx-0.3.0.dist-info/WHEEL +5 -0
- grimx-0.3.0.dist-info/entry_points.txt +2 -0
- grimx-0.3.0.dist-info/licenses/LICENSE +21 -0
- grimx-0.3.0.dist-info/top_level.txt +1 -0
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,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,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,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 "")
|
|
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,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
|