Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/tools/update-conda.py
7350 views
1
#!/usr/bin/env python3
2
# See README.md for more details
3
4
import argparse
5
import subprocess
6
import tomllib
7
from concurrent.futures import ThreadPoolExecutor
8
from pathlib import Path
9
10
from grayskull.config import Configuration
11
from grayskull.strategy.py_base import merge_setup_toml_metadata
12
from grayskull.strategy.py_toml import get_all_toml_info
13
from grayskull.strategy.pypi import extract_requirements, normalize_requirements_list
14
from packaging.requirements import Requirement
15
16
platforms = {
17
"linux-64": "linux",
18
"linux-aarch64": "linux-aarch64",
19
"osx-64": "macos-x86_64",
20
"osx-arm64": "macos",
21
"win-64": "win",
22
}
23
24
# Get source directory from command line arguments
25
parser = argparse.ArgumentParser()
26
parser.add_argument(
27
"sourcedir", help="Source directory", nargs="?", default=".", type=Path
28
)
29
parser.add_argument(
30
"-s",
31
"--systems",
32
help="Operating systems to build for; default is all",
33
nargs="+",
34
type=str,
35
choices=platforms.keys(),
36
)
37
options = parser.parse_args()
38
pythons = ["3.12", "3.13"]
39
tags = [""]
40
41
42
def write_env_file(env_file: Path, dependencies: list[str]) -> None:
43
env_file.write_text(
44
"""name: sage
45
channels:
46
- conda-forge
47
- nodefaults
48
dependencies:
49
"""
50
+ "".join(f" - {req}" + "\n" for req in dependencies)
51
)
52
print(f"Conda environment file written to {env_file}")
53
54
55
def filter_requirements(dependencies: set[str], python: str, platform: str) -> set[str]:
56
sys_platform = {
57
"linux-64": "linux",
58
"linux-aarch64": "linux",
59
"osx-64": "darwin",
60
"osx-arm64": "darwin",
61
"win-64": "win32",
62
}[platform]
63
platform_machine = {
64
"linux-64": "x86_64",
65
"linux-aarch64": "aarch64",
66
"osx-64": "x86_64",
67
"osx-arm64": "arm64",
68
"win-64": "x86_64",
69
}[platform]
70
env = {
71
"python_version": python,
72
"sys_platform": sys_platform,
73
"platform_machine": platform_machine,
74
}
75
76
def filter_dep(dep: str):
77
req = Requirement(dep)
78
if not req.marker or req.marker.evaluate(env):
79
# Serialize the requirement without the marker
80
req.marker = None
81
return str(req)
82
return None
83
84
return set(filter(None, map(filter_dep, dependencies)))
85
86
87
def update_conda(source_dir: Path, systems: list[str] | None) -> None:
88
pyproject_toml = source_dir / "pyproject.toml"
89
if not pyproject_toml.exists():
90
print(f"pyproject.toml not found in {pyproject_toml}")
91
return
92
93
def process_platform_python(platform_key, platform_value, python):
94
dependencies = get_dependencies(pyproject_toml, python, platform_key)
95
for tag in tags:
96
# Pin Python version
97
pinned_dependencies = {
98
f"python={python}" if dep == "python" else dep
99
for dep in dependencies
100
}
101
pinned_dependencies = sorted(pinned_dependencies)
102
103
env_file = source_dir / f"environment{tag}-{python}.yml"
104
write_env_file(env_file, pinned_dependencies)
105
lock_file = source_dir / f"environment{tag}-{python}-{platform_value}"
106
lock_file_gen = (
107
source_dir / f"environment{tag}-{python}-{platform_value}.yml"
108
)
109
print(
110
f"Updating lock file for {env_file} at {lock_file_gen}", flush=True
111
)
112
subprocess.run(
113
[
114
"conda-lock",
115
"--mamba",
116
"--channel",
117
"conda-forge",
118
"--kind",
119
"env",
120
"--platform",
121
platform_key,
122
"--file",
123
str(env_file),
124
"--lockfile",
125
str(lock_file),
126
"--filename-template",
127
str(lock_file),
128
],
129
check=True,
130
)
131
132
# Add conda env name to lock file at beginning
133
with open(lock_file_gen, "r+") as f:
134
content = f.read()
135
f.seek(0, 0)
136
f.write(f"name: sage{tag or '-dev'}\n{content}")
137
138
with ThreadPoolExecutor(max_workers=3) as executor:
139
futures = [
140
executor.submit(process_platform_python, platform_key, platform_value, python)
141
for platform_key, platform_value in platforms.items()
142
for python in pythons
143
if not (systems and platform_key not in systems)
144
]
145
for future in futures:
146
future.result()
147
148
def get_dependencies(pyproject_toml: Path, python: str, platform: str) -> set[str]:
149
grayskull_config = Configuration("sagemath")
150
with open(pyproject_toml, "rb") as f:
151
pyproject = tomllib.load(f)
152
pyproject_metadata = merge_setup_toml_metadata(
153
{}, get_all_toml_info(pyproject_toml)
154
)
155
requirements = extract_requirements(pyproject_metadata, grayskull_config, {})
156
all_requirements: set[str] = set(
157
requirements.get("build", [])
158
+ requirements.get("host", [])
159
+ requirements.get("run", [])
160
+ pyproject_metadata.get("install_requires", [])
161
+ get_dev_dependencies(pyproject)
162
+ get_optional_dependencies(pyproject)
163
)
164
165
# Fix requirements that are not available on conda
166
all_requirements = {
167
# Following can be removed once https://github.com/regro/cf-scripts/pull/2176 is used in grayskull
168
req.replace("lrcalc", "python-lrcalc")
169
.replace("symengine", "python-symengine")
170
.replace("memory_allocator", "memory-allocator")
171
.replace("pkg:generic/r-lattice", "r-lattice")
172
.replace("pkg:generic/latexmk", "latexmk")
173
.replace("pkg:generic/sagemath-elliptic-curves", "sagemath-db-elliptic-curves")
174
.replace("pkg:generic/sagemath-graphs", "sagemath-db-graphs")
175
.replace("pkg:generic/sagemath-polytopes-db", "sagemath-db-polytopes")
176
.replace("pkg:generic/tachyon", "tachyon")
177
.replace("brial", "libbrial") # on Conda, 'brial' refers to the Python package
178
for req in all_requirements
179
}
180
# Exclude requirements not available on conda (for a given platform)
181
exclude_packages: set[str] = {
182
"p_group_cohomology",
183
"sage_numerical_backends_coin",
184
"sagemath_giac",
185
"pynormaliz", # due to https://github.com/sagemath/sage/issues/40214
186
"latte-integrale", # due to https://github.com/sagemath/sage/issues/40216
187
}
188
if platform in ("linux-aarch64", "osx-arm64"):
189
exclude_packages |= {
190
"4ti2",
191
"latte-integrale",
192
"lrslib",
193
}
194
elif platform == "win-64":
195
exclude_packages |= {
196
"4ti2",
197
"bc",
198
"libbrial",
199
"bliss",
200
"cddlib",
201
"cliquer",
202
"ecl",
203
"eclib",
204
"ecm",
205
"fflas-ffpack",
206
"fplll",
207
"gap-defaults",
208
"gengetopt",
209
"gfan",
210
"giac",
211
"givaro",
212
"iml",
213
"latte-integrale",
214
"lcalc",
215
"libatomic_ops",
216
"libbraiding",
217
"libhomfly",
218
"linbox",
219
"lrcalc",
220
"lrslib",
221
"m4",
222
"m4rie",
223
"maxima",
224
"mpfi",
225
"ncurses",
226
"ntl",
227
"palp",
228
"patch",
229
"ppl",
230
"primecount",
231
"pynormaliz",
232
"python-lrcalc",
233
"readline",
234
"rpy2",
235
"rw",
236
"singular",
237
"sirocco",
238
"sympow",
239
"tachyon",
240
"tar",
241
"texinfo",
242
}
243
all_requirements = {
244
req
245
for req in all_requirements
246
if not any(
247
req == package or req.startswith(package + " ")
248
for package in exclude_packages
249
)
250
}
251
252
# Remove virtual packages to not confuse 'filter_requirements'
253
all_requirements.remove("{{ blas }}")
254
all_requirements.remove("{{ compiler('c') }}")
255
all_requirements.remove("{{ compiler('cxx') }}")
256
all_requirements.discard("<{ pin_compatible('numpy') }}")
257
# For some reason, grayskull mishandles the fortran compiler sometimes
258
# so handle both cases
259
for item in ["{{ compiler('fortran') }}", "{{ compiler'fortran' }}"]:
260
try:
261
all_requirements.remove(item)
262
except (ValueError, KeyError):
263
pass
264
for with_comment in {req for req in all_requirements if "#" in req}:
265
all_requirements.discard(with_comment)
266
267
all_requirements = filter_requirements(all_requirements, python, platform)
268
all_requirements = set(
269
normalize_requirements_list(list(all_requirements), grayskull_config)
270
)
271
# Specify concrete package for some virtual packages
272
if platform in ("osx-64", "osx-arm64"):
273
all_requirements.add("libblas=*=*_newaccelerate")
274
else:
275
all_requirements.add("openblas")
276
all_requirements.add("libblas=*=*_openblas")
277
all_requirements.add("fortran-compiler")
278
if platform == "win-64":
279
all_requirements.add("vs2022_win-64")
280
# For mingw:
281
# all_requirements.add("gcc_win-64 >= 14.2.0")
282
# all_requirements.add("gxx_win-64")
283
else:
284
all_requirements.add("c-compiler")
285
all_requirements.add("cxx-compiler")
286
287
# Add additional dependencies based on platform
288
if platform == "win-64":
289
# Flint needs pthread.h
290
all_requirements.add("winpthreads-devel")
291
# Workaround for https://github.com/conda-forge/libpng-feedstock/issues/47
292
all_requirements.add("zlib")
293
if platform != "win-64":
294
# Needed to run configure/bootstrap, can be deleted once we fully migrated to meson
295
all_requirements.add("autoconf")
296
all_requirements.add("automake")
297
all_requirements.add("m4")
298
# Needed to fix a bug on Macos with broken pkg-config
299
all_requirements.add("expat")
300
301
# Packages with version constraints
302
# https://github.com/sagemath/sage/pull/40679
303
if platform != "win-64":
304
all_requirements.remove("maxima")
305
all_requirements.add("maxima < 5.48.0")
306
307
return all_requirements
308
309
310
def get_dev_dependencies(pyproject: dict) -> list[str]:
311
dependency_groups = pyproject.get("dependency-groups", {})
312
dev_dependencies = (
313
dependency_groups.get("test", [])
314
+ dependency_groups.get("docs", [])
315
+ dependency_groups.get("lint", [])
316
+ dependency_groups.get("dev", [])
317
)
318
# Remove dependencies that are not available on conda
319
dev_dependencies.remove("relint")
320
return dev_dependencies
321
322
323
def get_optional_dependencies(pyproject: dict) -> list[str]:
324
optional_dependencies = []
325
optional_groups = pyproject.get("project", {}).get("optional-dependencies", {})
326
for _, dependencies in optional_groups.items():
327
optional_dependencies.extend(dependencies)
328
# print(f"Optional dependencies: {optional_dependencies}") # Uncommented for debugging
329
return optional_dependencies
330
331
332
update_conda(options.sourcedir, options.systems)
333
334