cola-cli 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.
- cola_cli/__init__.py +3 -0
- cola_cli/__main__.py +3 -0
- cola_cli/builders.py +347 -0
- cola_cli/cli.py +177 -0
- cola_cli/common.py +98 -0
- cola_cli/environment.py +245 -0
- cola_cli/runtime.py +25 -0
- cola_cli/scripts/install.jl +10 -0
- cola_cli/scripts/locking.gradle +9 -0
- cola_cli/setup.py +163 -0
- cola_cli/sources.py +256 -0
- cola_cli/templates/common/.editorconfig.j2 +15 -0
- cola_cli/templates/common/.gitignore.j2 +20 -0
- cola_cli/templates/common/cola.toml.j2 +8 -0
- cola_cli/templates/cpp/.clang-format.j2 +6 -0
- cola_cli/templates/cpp/.clang-tidy.j2 +4 -0
- cola_cli/templates/cpp/CMakeLists.txt.j2 +51 -0
- cola_cli/templates/cpp/include/{{ name }}.hh.j2 +23 -0
- cola_cli/templates/cpp/src/{{ name }}.cc.j2 +3 -0
- cola_cli/templates/fortran/.fprettify.rc.j2 +2 -0
- cola_cli/templates/fortran/CMakeLists.txt.j2 +63 -0
- cola_cli/templates/fortran/src/{{ name }}.f90.j2 +73 -0
- cola_cli/templates/java/CMakeLists.txt.j2 +33 -0
- cola_cli/templates/java/build.gradle.kts.j2 +35 -0
- cola_cli/templates/java/config/checkstyle/checkstyle.xml.j2 +19 -0
- cola_cli/templates/java/settings.gradle.kts.j2 +1 -0
- cola_cli/templates/java/src/main/java/org/cola/generated/{{ package }}/{{ name }}Converter.java.j2 +9 -0
- cola_cli/templates/java/src/main/java/org/cola/generated/{{ package }}/{{ name }}Generator.java.j2 +9 -0
- cola_cli/templates/java/src/main/java/org/cola/generated/{{ package }}/{{ name }}Writer.java.j2 +9 -0
- cola_cli/templates/julia/.JuliaFormatter.toml.j2 +2 -0
- cola_cli/templates/julia/Project.toml.j2 +10 -0
- cola_cli/templates/julia/src/{{ name }}.jl.j2 +17 -0
- cola_cli/templates/python/pyproject.toml.j2 +23 -0
- cola_cli/templates/python/src/{{ package }}/__init__.py.j2 +1 -0
- cola_cli/templates/python/src/{{ package }}/filters.py.j2 +18 -0
- cola_cli/templates/quality/.cmake-format.yaml.j2 +13 -0
- cola_cli/templates/quality/CMakePresets.json.j2 +22 -0
- cola_cli-0.1.0.dist-info/METADATA +272 -0
- cola_cli-0.1.0.dist-info/RECORD +43 -0
- cola_cli-0.1.0.dist-info/WHEEL +5 -0
- cola_cli-0.1.0.dist-info/entry_points.txt +2 -0
- cola_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- cola_cli-0.1.0.dist-info/top_level.txt +1 -0
cola_cli/environment.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""A single per-user environment, with atomic activation of completed builds."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import uuid
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .common import ColaError, read_json, write_json
|
|
15
|
+
from .sources import BUILTINS, describe, ordered, snapshot, source_spec, tree_hash
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fingerprint(value):
|
|
19
|
+
return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def home():
|
|
23
|
+
if sys.platform not in {"linux", "darwin"}:
|
|
24
|
+
raise ColaError("cola-cli supports Linux and macOS only.")
|
|
25
|
+
# Intentionally never inspect the current directory for an environment.
|
|
26
|
+
path = Path(os.environ.get("COLA_HOME", "~/.local/share/cola")).expanduser()
|
|
27
|
+
if not path.is_absolute():
|
|
28
|
+
raise ColaError("COLA_HOME must be an absolute path to the global store.")
|
|
29
|
+
return path.resolve()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GlobalEnvironment:
|
|
33
|
+
def __init__(self, root=None):
|
|
34
|
+
self.root = Path(root) if root else home()
|
|
35
|
+
self.manifest = self.root / "global.json"
|
|
36
|
+
|
|
37
|
+
@contextlib.contextmanager
|
|
38
|
+
def writing(self):
|
|
39
|
+
import fcntl
|
|
40
|
+
|
|
41
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
with (self.root / ".write.lock").open("a") as stream:
|
|
43
|
+
try:
|
|
44
|
+
fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
45
|
+
except BlockingIOError as error:
|
|
46
|
+
raise ColaError(
|
|
47
|
+
"Another cola command is modifying the global environment. Try again when it finishes."
|
|
48
|
+
) from error
|
|
49
|
+
try:
|
|
50
|
+
yield
|
|
51
|
+
finally:
|
|
52
|
+
fcntl.flock(stream, fcntl.LOCK_UN)
|
|
53
|
+
|
|
54
|
+
def spec(self):
|
|
55
|
+
if not self.manifest.exists():
|
|
56
|
+
raise ColaError("Global environment is not initialized. Run 'cola init' first.")
|
|
57
|
+
spec = read_json(self.manifest)
|
|
58
|
+
if spec.get("schema") != 1:
|
|
59
|
+
raise ColaError("Unsupported global environment schema.")
|
|
60
|
+
return spec
|
|
61
|
+
|
|
62
|
+
def initialize(self, core, bindings, python, prefixes=()):
|
|
63
|
+
if self.manifest.exists():
|
|
64
|
+
raise ColaError(f"The global environment already exists at {self.root}.")
|
|
65
|
+
modules = {}
|
|
66
|
+
for role, source in (("core", core), ("python", bindings)):
|
|
67
|
+
modules[role] = self.builtin(role, source)
|
|
68
|
+
interpreter = shutil.which(python)
|
|
69
|
+
if interpreter is None:
|
|
70
|
+
raise ColaError(f"Python executable not found: {python}")
|
|
71
|
+
write_json(
|
|
72
|
+
self.manifest,
|
|
73
|
+
{
|
|
74
|
+
"schema": 1,
|
|
75
|
+
"python": str(Path(interpreter).absolute()),
|
|
76
|
+
"prefixes": [str(Path(path).expanduser().resolve()) for path in prefixes],
|
|
77
|
+
"modules": modules,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def builtin(role, source=None):
|
|
83
|
+
libraries = {"core": [], "python": ["COLA-Py"], "fortran": [], "java": ["COLA_JVM"], "julia": ["COLA_Julia"]}
|
|
84
|
+
return {
|
|
85
|
+
"name": role,
|
|
86
|
+
"role": role,
|
|
87
|
+
"backend": {"python": "python", "julia": "julia"}.get(role, "cmake"),
|
|
88
|
+
"depends": [] if role == "core" else ["core"],
|
|
89
|
+
"libraries": libraries[role],
|
|
90
|
+
"cmake_args": [],
|
|
91
|
+
"source": source or source_spec(BUILTINS[role]),
|
|
92
|
+
"options": {},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def add(self, source, **options):
|
|
96
|
+
spec = self.spec()
|
|
97
|
+
frozen = snapshot(self.root, source)
|
|
98
|
+
module = describe(self.root / "sources" / frozen["sha256"], **options)
|
|
99
|
+
module.update(source=source, snapshot=frozen, options=options)
|
|
100
|
+
name = module["name"]
|
|
101
|
+
spec["modules"][name] = module
|
|
102
|
+
for dependency in module["depends"]:
|
|
103
|
+
if dependency in BUILTINS and dependency not in spec["modules"]:
|
|
104
|
+
spec["modules"][dependency] = self.builtin(dependency)
|
|
105
|
+
ordered(spec["modules"])
|
|
106
|
+
write_json(self.manifest, spec)
|
|
107
|
+
return name
|
|
108
|
+
|
|
109
|
+
def remove(self, name):
|
|
110
|
+
spec = self.spec()
|
|
111
|
+
if name in {"core", "python"}:
|
|
112
|
+
raise ColaError("The core and Python runtime are required by cola run.")
|
|
113
|
+
if name not in spec["modules"]:
|
|
114
|
+
raise ColaError(f"Module {name!r} is not installed in the global specification.")
|
|
115
|
+
dependents = [key for key, module in spec["modules"].items() if name in module["depends"]]
|
|
116
|
+
if dependents:
|
|
117
|
+
raise ColaError(f"{name} is required by: {', '.join(dependents)}")
|
|
118
|
+
del spec["modules"][name]
|
|
119
|
+
write_json(self.manifest, spec)
|
|
120
|
+
|
|
121
|
+
def current(self):
|
|
122
|
+
current = self.root / "current"
|
|
123
|
+
if not current.is_symlink():
|
|
124
|
+
raise ColaError("No completed global build. Run 'cola build' first.")
|
|
125
|
+
generation = current.resolve()
|
|
126
|
+
if not generation.is_relative_to((self.root / "generations").resolve()):
|
|
127
|
+
raise ColaError("Global environment pointer refers outside its generation directory.")
|
|
128
|
+
return generation, read_json(generation / "cola.lock")
|
|
129
|
+
|
|
130
|
+
def prepare(self, spec, *, locked=False, update=()):
|
|
131
|
+
if locked:
|
|
132
|
+
_, previous = self.current()
|
|
133
|
+
if previous["spec_digest"] != fingerprint(spec):
|
|
134
|
+
raise ColaError("The global specification changed. Run 'cola build' to create a new lock.")
|
|
135
|
+
modules = previous["modules"]
|
|
136
|
+
else:
|
|
137
|
+
modules = {}
|
|
138
|
+
for name in ordered(spec["modules"]):
|
|
139
|
+
wanted = spec["modules"][name]
|
|
140
|
+
source = wanted["source"]
|
|
141
|
+
frozen = wanted.get("snapshot")
|
|
142
|
+
if source["kind"] == "path" or name in update or not frozen:
|
|
143
|
+
frozen = snapshot(self.root, source)
|
|
144
|
+
cached = self.root / "sources" / frozen["sha256"]
|
|
145
|
+
if not cached.exists() or tree_hash(cached) != frozen["sha256"]:
|
|
146
|
+
raise ColaError(f"Missing or modified source snapshot for {name}. Use 'cola update {name}'.")
|
|
147
|
+
module = describe(cached, **wanted.get("options", {}))
|
|
148
|
+
if module["name"] != name:
|
|
149
|
+
raise ColaError(f"Source for {name} identifies itself as {module['name']}.")
|
|
150
|
+
module.update(source=source, snapshot=frozen, options=wanted.get("options", {}))
|
|
151
|
+
modules[name] = module
|
|
152
|
+
ordered(modules)
|
|
153
|
+
for name, module in modules.items():
|
|
154
|
+
cached = self.root / "sources" / module["snapshot"]["sha256"]
|
|
155
|
+
if not cached.is_dir() or tree_hash(cached) != module["snapshot"]["sha256"]:
|
|
156
|
+
raise ColaError(f"Missing or modified locked snapshot for {name}: {cached}")
|
|
157
|
+
return modules
|
|
158
|
+
|
|
159
|
+
def build(self, *, locked=False, update=(), jobs=2):
|
|
160
|
+
from .builders import Builder
|
|
161
|
+
|
|
162
|
+
spec = self.spec()
|
|
163
|
+
unknown = set(update) - set(spec["modules"])
|
|
164
|
+
if unknown:
|
|
165
|
+
raise ColaError(f"Unknown modules: {', '.join(sorted(unknown))}")
|
|
166
|
+
modules = self.prepare(spec, locked=locked, update=update)
|
|
167
|
+
previous = self.current()[1] if locked else None
|
|
168
|
+
generation = self.root / "generations" / uuid.uuid4().hex
|
|
169
|
+
generation.mkdir(parents=True)
|
|
170
|
+
for name, module in modules.items():
|
|
171
|
+
shutil.copytree(self.root / "sources" / module["snapshot"]["sha256"], generation / "src" / name)
|
|
172
|
+
builder = Builder(self.root, generation, spec, modules, previous, jobs)
|
|
173
|
+
try:
|
|
174
|
+
toolchain = builder.toolchain()
|
|
175
|
+
if previous and toolchain != previous["toolchain"]:
|
|
176
|
+
raise ColaError(
|
|
177
|
+
"Toolchain differs from the active lock. Run 'cola build' to accept and rebuild with it."
|
|
178
|
+
)
|
|
179
|
+
locks = builder.build()
|
|
180
|
+
result = {
|
|
181
|
+
"schema": 1,
|
|
182
|
+
"spec_digest": fingerprint(spec),
|
|
183
|
+
"modules": modules,
|
|
184
|
+
"toolchain": toolchain,
|
|
185
|
+
"locks": locks,
|
|
186
|
+
"platform": {"system": platform.system(), "machine": platform.machine()},
|
|
187
|
+
}
|
|
188
|
+
builder.check()
|
|
189
|
+
# Resolve the desired specification before publishing the completed generation.
|
|
190
|
+
# A failure here can leave pending desired edits, but cannot replace the active runtime.
|
|
191
|
+
for name, module in modules.items():
|
|
192
|
+
spec["modules"][name]["snapshot"] = module["snapshot"]
|
|
193
|
+
result["spec_digest"] = fingerprint(spec)
|
|
194
|
+
result["spec"] = spec
|
|
195
|
+
write_json(generation / "cola.lock", result)
|
|
196
|
+
write_json(self.manifest, spec)
|
|
197
|
+
# All absolute prefixes remain valid: activate a pointer, never move an installed tree.
|
|
198
|
+
pointer = self.root / (".current-" + uuid.uuid4().hex)
|
|
199
|
+
pointer.symlink_to(generation.relative_to(self.root), target_is_directory=True)
|
|
200
|
+
os.replace(pointer, self.root / "current")
|
|
201
|
+
return generation
|
|
202
|
+
except BaseException as error:
|
|
203
|
+
# Failed generations remain inspectable but can never become current.
|
|
204
|
+
if isinstance(error, ColaError):
|
|
205
|
+
raise ColaError(f"{error}\nBuild was not activated. Log: {generation / 'build.log'}") from error
|
|
206
|
+
raise
|
|
207
|
+
|
|
208
|
+
def runtime_environment(self):
|
|
209
|
+
from .builders import managed_environment
|
|
210
|
+
|
|
211
|
+
generation, lock = self.current()
|
|
212
|
+
env = managed_environment(self.root, generation, lock["spec"])
|
|
213
|
+
# The active build owns its native stack even when the desired spec has pending edits.
|
|
214
|
+
env["PATH"] = os.pathsep.join(
|
|
215
|
+
[
|
|
216
|
+
str(generation / "python" / ".venv" / "bin"),
|
|
217
|
+
str(generation / "prefix" / "bin"),
|
|
218
|
+
env["PATH"],
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
env.pop("COLA_BUILD_LOG", None)
|
|
222
|
+
return generation, lock, env
|
|
223
|
+
|
|
224
|
+
def run(self, config, libraries, steps):
|
|
225
|
+
generation, lock, env = self.runtime_environment()
|
|
226
|
+
selected = list(libraries) or list(
|
|
227
|
+
dict.fromkeys(
|
|
228
|
+
library for name in ordered(lock["modules"]) for library in lock["modules"][name]["libraries"]
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
runtime = Path(__file__).with_name("runtime.py")
|
|
232
|
+
return subprocess.call(
|
|
233
|
+
[
|
|
234
|
+
generation / "python" / ".venv" / "bin" / "python",
|
|
235
|
+
"-I",
|
|
236
|
+
runtime,
|
|
237
|
+
"--config",
|
|
238
|
+
Path(config).expanduser().resolve(),
|
|
239
|
+
"--steps",
|
|
240
|
+
str(steps),
|
|
241
|
+
"--libraries",
|
|
242
|
+
json.dumps(selected),
|
|
243
|
+
],
|
|
244
|
+
env=env,
|
|
245
|
+
)
|
cola_cli/runtime.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Executed by the managed Python, never imported by the CLI interpreter."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser()
|
|
9
|
+
parser.add_argument("--config")
|
|
10
|
+
parser.add_argument("--steps", type=int, default=1)
|
|
11
|
+
parser.add_argument("--libraries", required=True)
|
|
12
|
+
parser.add_argument("--check", action="store_true")
|
|
13
|
+
args = parser.parse_args()
|
|
14
|
+
from colapy import RunManager
|
|
15
|
+
|
|
16
|
+
manager = RunManager()
|
|
17
|
+
for library in json.loads(args.libraries):
|
|
18
|
+
manager.load_module(library)
|
|
19
|
+
if not args.check:
|
|
20
|
+
manager.load_config(file=args.config)
|
|
21
|
+
manager.run(args.steps)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main()
|
cola_cli/setup.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Generate COLA project scaffolds using the shipped language templates."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import uuid
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from jinja2 import Environment, StrictUndefined
|
|
9
|
+
|
|
10
|
+
from .common import cli
|
|
11
|
+
|
|
12
|
+
TEMPLATES = Path(__file__).with_name("templates")
|
|
13
|
+
LANGUAGES = click.Choice(["cpp", "python", "fortran", "java", "julia"], case_sensitive=False)
|
|
14
|
+
JULIA_RESERVED_NAMES = set(
|
|
15
|
+
"baremodule begin break catch const continue do else elseif end export false finally for function "
|
|
16
|
+
"global if import let local macro module quote return struct true try using while "
|
|
17
|
+
"abstract mutable primitive type where in isa public outer as COLA Core Base".split()
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def project_name(ctx, param, value):
|
|
22
|
+
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,31}", value):
|
|
23
|
+
raise click.BadParameter("use 1–32 ASCII letters, digits or underscores, starting with a letter")
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def project_version(ctx, param, value):
|
|
28
|
+
if not re.fullmatch(r"\d+\.\d+\.\d+", value):
|
|
29
|
+
raise click.BadParameter("use a numeric major.minor.patch version, e.g. 1.0.0")
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_project(name, language, version):
|
|
34
|
+
environment = Environment(undefined=StrictUndefined, keep_trailing_newline=True, autoescape=False)
|
|
35
|
+
context = {
|
|
36
|
+
"name": name,
|
|
37
|
+
"package": name.lower() + "_filters",
|
|
38
|
+
"version": version,
|
|
39
|
+
"language": language,
|
|
40
|
+
"uuid": str(uuid.uuid4()),
|
|
41
|
+
}
|
|
42
|
+
groups = ["common"]
|
|
43
|
+
if language in ("cpp", "fortran", "java"):
|
|
44
|
+
groups.append("quality")
|
|
45
|
+
groups.append(language)
|
|
46
|
+
files = {}
|
|
47
|
+
for group in groups:
|
|
48
|
+
directory = TEMPLATES / group
|
|
49
|
+
for template in sorted(directory.rglob("*.j2")):
|
|
50
|
+
path = environment.from_string(template.relative_to(directory).as_posix()[:-3]).render(context)
|
|
51
|
+
files[path] = environment.from_string(template.read_text(encoding="utf-8")).render(context)
|
|
52
|
+
return files
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def write_project(name, prefix, version, language, force, section=None):
|
|
56
|
+
if language == "julia" and name in JULIA_RESERVED_NAMES:
|
|
57
|
+
raise click.ClickException(f"Reserved Julia module name: {name}")
|
|
58
|
+
files = render_project(name, language, version)
|
|
59
|
+
if section == "cmake":
|
|
60
|
+
if language in ("python", "julia"):
|
|
61
|
+
build_file = "pyproject.toml" if language == "python" else "Project.toml"
|
|
62
|
+
raise click.ClickException(
|
|
63
|
+
f"{language} projects use {build_file}; use 'setup project --language {language}'."
|
|
64
|
+
)
|
|
65
|
+
files = {p: text for p, text in files.items() if "cmake" in p.lower() or p == "CMakeLists.txt"}
|
|
66
|
+
elif section == "sources":
|
|
67
|
+
files = {p: text for p, text in files.items() if p.startswith(("src/", "include/"))}
|
|
68
|
+
elif section == "git":
|
|
69
|
+
files = {".gitignore": files[".gitignore"]}
|
|
70
|
+
root = Path(prefix).expanduser() / name
|
|
71
|
+
if root.is_symlink() or (root.exists() and not root.is_dir()):
|
|
72
|
+
raise click.ClickException(f"Project path is not a regular directory: {root}")
|
|
73
|
+
if section is None and root.exists() and any(root.iterdir()) and not force:
|
|
74
|
+
raise click.ClickException(f"Directory is not empty: {root}. Use --force to overwrite generated files.")
|
|
75
|
+
# Check all destinations before writing, including symlinks inside an existing project.
|
|
76
|
+
for relative in files:
|
|
77
|
+
target = root / relative
|
|
78
|
+
for component in (target, *target.parents):
|
|
79
|
+
if component == root.parent:
|
|
80
|
+
break
|
|
81
|
+
if component.is_symlink():
|
|
82
|
+
raise click.ClickException(f"Refusing to write through a symlink: {component}")
|
|
83
|
+
if component != target and component.exists() and not component.is_dir():
|
|
84
|
+
raise click.ClickException(f"Not a directory: {component}")
|
|
85
|
+
if target.exists() and (target.is_dir() or not force):
|
|
86
|
+
raise click.ClickException(f"File already exists: {target}. Use --force to overwrite generated files.")
|
|
87
|
+
try:
|
|
88
|
+
if language == "julia" and "Project.toml" in files and (root / "Project.toml").is_file():
|
|
89
|
+
header = (root / "Project.toml").read_text(encoding="utf-8").split("[", 1)[0]
|
|
90
|
+
match = re.search(r'^uuid\s*=\s*"([^"]+)"', header, re.MULTILINE)
|
|
91
|
+
if match:
|
|
92
|
+
try:
|
|
93
|
+
project_uuid = str(uuid.UUID(match[1]))
|
|
94
|
+
except ValueError as error:
|
|
95
|
+
raise click.ClickException("Invalid UUID in existing Project.toml") from error
|
|
96
|
+
files["Project.toml"] = re.sub(
|
|
97
|
+
r'^uuid = ".*"', f'uuid = "{project_uuid}"', files["Project.toml"], flags=re.MULTILINE
|
|
98
|
+
)
|
|
99
|
+
for relative, content in files.items():
|
|
100
|
+
target = root / relative
|
|
101
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
target.write_text(content, encoding="utf-8")
|
|
103
|
+
except OSError as error:
|
|
104
|
+
raise click.ClickException(str(error)) from error
|
|
105
|
+
click.echo(f"Created {language} project files in {root}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def options(function):
|
|
109
|
+
for decorator in (
|
|
110
|
+
click.option("--force", is_flag=True, help="Overwrite generated files; keep unrelated files."),
|
|
111
|
+
click.option("--version", default="1.0.0", callback=project_version, show_default=True),
|
|
112
|
+
click.option(
|
|
113
|
+
"--prefix",
|
|
114
|
+
default="./",
|
|
115
|
+
type=click.Path(file_okay=False),
|
|
116
|
+
show_default=True,
|
|
117
|
+
help="Parent directory for the new project.",
|
|
118
|
+
),
|
|
119
|
+
click.option("--language", "-l", type=LANGUAGES, default="cpp", show_default=True),
|
|
120
|
+
click.option("--name", required=True, callback=project_name, help="Project/module name."),
|
|
121
|
+
):
|
|
122
|
+
function = decorator(function)
|
|
123
|
+
return function
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@cli.group()
|
|
127
|
+
def setup():
|
|
128
|
+
"""Create COLA projects and individual project components."""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@setup.command()
|
|
132
|
+
@options
|
|
133
|
+
def project(**kwargs):
|
|
134
|
+
"""Create a C++, Python, Fortran, Java or Julia project scaffold."""
|
|
135
|
+
write_project(**kwargs)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@setup.command()
|
|
139
|
+
@options
|
|
140
|
+
def cmake(**kwargs):
|
|
141
|
+
"""Generate just the native build files (legacy command)."""
|
|
142
|
+
write_project(**kwargs, section="cmake")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@setup.command()
|
|
146
|
+
@options
|
|
147
|
+
def sources(**kwargs):
|
|
148
|
+
"""Generate just the imports and filter declarations."""
|
|
149
|
+
write_project(**kwargs, section="sources")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@setup.command()
|
|
153
|
+
@options
|
|
154
|
+
def git(**kwargs):
|
|
155
|
+
"""Generate .gitignore without initializing a repository."""
|
|
156
|
+
write_project(**kwargs, section="git")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@cli.command()
|
|
160
|
+
def version():
|
|
161
|
+
from cola_cli import __version__
|
|
162
|
+
|
|
163
|
+
click.echo(f"COLA CLI {__version__}")
|