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