-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathutil.py
296 lines (241 loc) · 9.84 KB
/
util.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import tomllib
import argparse
import os
import subprocess
import shutil
import glob
from typing import Any
TEMP_DIR = os.path.expandvars("$HOME/tmp")
RECIPE_DIR = "./src"
def build_dependency_list(dependencies: dict[str, str]) -> list[str]:
"""Converts the list of dependencies from the mojoproject.toml into a list of strings for the recipe."""
deps: list[str] = []
for name, version in dependencies.items():
start = 0
operator = "=="
if version[0] in {"<", ">"}:
if version[1] != "=":
operator = version[0]
start = 1
else:
operator = version[:2]
start = 2
elif version.startswith("=="):
start = 2
deps.append(f" - {name} {operator} {version[start:]}")
return deps
def load_project_config() -> dict[str, Any]:
"""Loads the project configuration from the mojoproject.toml file."""
with open("mojoproject.toml", "rb") as f:
return tomllib.load(f)
def generate_recipe(args: Any) -> None:
"""Generates a recipe for the project based on the project configuration in the mojoproject.toml."""
# Load the project configuration and recipe template.
config: dict[str, Any] = load_project_config()
recipe: str
with open("src/recipe.tmpl", "r") as f:
recipe = f.read()
# Replace the placeholders in the recipe with the project configuration.
recipe = (
recipe.replace("{{NAME}}", config["project"]["name"])
.replace("{{DESCRIPTION}}", config["project"]["description"])
.replace("{{LICENSE}}", config["project"]["license"])
.replace("{{LICENSE_FILE}}", config["project"]["license-file"])
.replace("{{HOMEPAGE}}", config["project"]["homepage"])
.replace("{{REPOSITORY}}", config["project"]["repository"])
.replace("{{VERSION}}", config["project"]["version"])
)
if args.mode != "default":
recipe = recipe.replace("{{ENVIRONMENT_FLAG}}", f"-e {args.mode}")
else:
recipe = recipe.replace("{{ENVIRONMENT_FLAG}}", "")
# Dependencies are the only notable field that changes between environments.
dependencies: dict[str, str]
match args.mode:
case "default":
dependencies = config["dependencies"]
case _:
dependencies = config["feature"][args.mode]["dependencies"]
deps = build_dependency_list(dependencies)
recipe = recipe.replace("{{DEPENDENCIES}}", "\n".join(deps))
# Write the final recipe.
with open("src/recipe.yaml", "w+") as f:
recipe = f.write(recipe)
def publish_to_prefix(args: Any) -> None:
"""Publishes the conda packages to the specified conda channel."""
# If CONDA_BLD_PATH is set, then pubilsh from there. Otherwise, publish from the current directory.
conda_build_path = os.environ.get("CONDA_BLD_PATH", os.getcwd())
if not conda_build_path:
raise ValueError("CONDA_BLD_PATH environment variable is not set. This ")
print(f"Publishing packages to: {args.channel}")
for file in glob.glob(f'{conda_build_path}/**/*.conda'):
try:
subprocess.run(
["rattler-build", "upload", "prefix", "-c", args.channel, file],
check=True,
)
except subprocess.CalledProcessError:
pass
os.remove(file)
def remove_temp_directory() -> None:
"""Removes the temporary directory used for building the package."""
if os.path.exists(TEMP_DIR):
print("Removing temp directory.")
shutil.rmtree(TEMP_DIR)
def prepare_temp_directory() -> None:
"""Creates the temporary directory used for building the package. Adds the compiled mojo package to the directory."""
package = load_project_config()["project"]["name"]
remove_temp_directory()
os.mkdir(TEMP_DIR)
subprocess.run(
["mojo", "package", f"src/{package}", "-o", f"{TEMP_DIR}/{package}.mojopkg"],
check=True,
)
def execute_package_tests(args: Any) -> None:
"""Executes the tests for the package."""
TEST_DIR = "./test"
print("Building package and copying tests.")
prepare_temp_directory()
shutil.copytree(TEST_DIR, TEMP_DIR, dirs_exist_ok=True)
target = TEMP_DIR
if args.path:
target = f"{target}/{args.path}"
print(f"Running tests at {target}...")
subprocess.run(["mojo", "test", target], check=True)
remove_temp_directory()
def execute_package_examples(args: Any) -> None:
"""Executes the examples for the package."""
EXAMPLE_DIR = "examples"
if not os.path.exists("examples"):
print(f"Path does not exist: {EXAMPLE_DIR}.")
return
print("Building package and copying examples.")
prepare_temp_directory()
shutil.copytree(EXAMPLE_DIR, TEMP_DIR, dirs_exist_ok=True)
example_files = f'{EXAMPLE_DIR}/*.mojo'
if args.path:
example_files = f"{EXAMPLE_DIR}/{args.path}"
print(f"Running examples in {example_files}...")
for file in glob.glob(example_files):
file_name = os.path.basename(file)
name, _ = os.path.splitext(file_name)
shutil.copyfile(file, f"{TEMP_DIR}/{file_name}")
subprocess.run(["mojo", "build", f"{TEMP_DIR}/{file_name}", "-o", f"{TEMP_DIR}/{name}"], check=True)
subprocess.run([f"{TEMP_DIR}/{name}"], check=True)
remove_temp_directory()
def execute_package_benchmarks(args: Any) -> None:
BENCHMARK_DIR = "./benchmarks"
if not os.path.exists("benchmarks"):
print(f"Path does not exist: {BENCHMARK_DIR}.")
return
print("Building package and copying benchmarks.")
prepare_temp_directory()
shutil.copytree(BENCHMARK_DIR, TEMP_DIR, dirs_exist_ok=True)
benchmark_files = f'{BENCHMARK_DIR}/*.mojo'
if args.path:
benchmark_files = f"{BENCHMARK_DIR}/{args.path}"
print(f"Running benchmarks in {benchmark_files}...")
for file in glob.glob(benchmark_files):
file_name = os.path.basename(file)
name, _ = os.path.splitext(file_name)
shutil.copyfile(file, f"{TEMP_DIR}/{file_name}")
subprocess.run(["mojo", "build", f"{TEMP_DIR}/{file_name}", "-o", f"{TEMP_DIR}/{name}"], check=True)
subprocess.run([f"{TEMP_DIR}/{name}"], check=True)
remove_temp_directory()
def build_conda_package(args: Any) -> None:
"""Builds the conda package for the project."""
# Build the conda package for the project.
config = load_project_config()
channels: list[str]
rattler_command: list[str]
match args.mode:
case "default":
channels = config["project"]["channels"]
rattler_command = ["magic", "run", "rattler-build", "build"]
case _:
channels = config["feature"][args.mode]["channels"]
rattler_command = ["magic", "run", "-e", args.mode, "rattler-build", "build"]
options: list[str] = []
for channel in channels:
options.extend(["-c", channel])
generate_recipe(args)
subprocess.run(
[*rattler_command, "-r", RECIPE_DIR, "--skip-existing=all", *options],
check=True,
)
os.remove(f"{RECIPE_DIR}/recipe.yaml")
def main():
# Configure the parser to receive the mode argument.
# create the top-level parser
parser = argparse.ArgumentParser(
prog="util", description="Generate a recipe for the project."
)
subcommands = parser.add_subparsers(help="sub-command help")
# create the parser for the "templater" command
templater = subcommands.add_parser("templater", help="template help")
templater.add_argument(
"-m",
"--mode",
type=str,
default="default",
help="The environment to generate the recipe for. Defaults to 'default' for the standard version.",
)
templater.set_defaults(func=generate_recipe)
# create the parser for the "build" command
build = subcommands.add_parser("build", help="build help")
build.add_argument(
"-m",
"--mode",
type=str,
default="default",
help="The environment to build the package using. Defaults to 'default' for the standard version.",
)
build.set_defaults(func=build_conda_package)
# create the parser for the "publish" command
publish = subcommands.add_parser("publish", help="publish help")
publish.add_argument(
"-c",
"--channel",
type=str,
default="mojo-community",
help="The prefix.dev conda channel to publish to. Defaults to 'mojo-community'.",
)
publish.set_defaults(func=publish_to_prefix)
# create the parser for the "run" command
run = subcommands.add_parser("run", help="run help")
run_subcommands = run.add_subparsers(help="run sub-command help")
# create the parser for the "run tests" command
run_tests = run_subcommands.add_parser("tests", help="tests help")
run_tests.add_argument(
"-p",
"--path",
type=str,
default=None,
help="Optional path to test file or test directory to run tests for.",
)
run_tests.set_defaults(func=execute_package_tests)
# create the parser for the "run benchmarks" command
run_benchmarks = run_subcommands.add_parser("benchmarks", help="benchmarks help")
run_benchmarks.add_argument(
"-p",
"--path",
type=str,
default=None,
help="Optional path to benchmark file or test directory to run tests for.",
)
run_benchmarks.set_defaults(func=execute_package_benchmarks)
# create the parser for the "run examples" command
run_examples = run_subcommands.add_parser("examples", help="examples help")
run_examples.add_argument(
"-p",
"--path",
type=str,
default=None,
help="Optional path to example file or test directory to run tests for.",
)
run_examples.set_defaults(func=execute_package_examples)
args = parser.parse_args()
if args.func:
args.func(args)
if __name__ == "__main__":
main()