Merge branch 'main' into am/build-locally-executable

This commit is contained in:
Austin Morton
2025-05-02 15:40:42 -05:00
committed by GitHub
20 changed files with 581 additions and 1018 deletions
+104 -74
View File
@@ -26,8 +26,10 @@ except ImportError:
EXAMPLE_RECIPE_FOLDERS = ["example", "example-v1"]
LOCAL_CHANNELS = os.environ.get("CONDA_BLD_PATH", "local").split(",")
def get_host_platform():
from sys import platform
if platform == "linux" or platform == "linux2":
return "linux"
elif platform == "darwin":
@@ -54,7 +56,9 @@ def build_all(recipes_dir, arch):
platform = get_host_platform()
script_dir = os.path.dirname(os.path.realpath(__file__))
variant_config_file = os.path.join(script_dir, "{}.yaml".format(get_config_name(arch)))
variant_config_file = os.path.join(
script_dir, "{}.yaml".format(get_config_name(arch))
)
has_meta_yaml = False
has_recipe_yaml = False
@@ -65,11 +69,11 @@ def build_all(recipes_dir, arch):
meta_yaml = os.path.join(recipes_dir, folder, "meta.yaml")
if os.path.exists(meta_yaml):
has_meta_yaml = True
with(open(meta_yaml, "r", encoding="utf-8")) as f:
text = ''.join(f.readlines())
if 'cuda' in text:
with open(meta_yaml, "r", encoding="utf-8") as f:
text = "".join(f.readlines())
if "cuda" in text:
found_cuda = True
if 'sysroot_linux-64' in text:
if "sysroot_linux-64" in text:
found_centos7 = True
recipe_yaml = os.path.join(recipes_dir, folder, "recipe.yaml")
@@ -86,16 +90,18 @@ def build_all(recipes_dir, arch):
if os.path.exists(cbc):
with open(cbc, "r") as f:
lines = f.readlines()
pat = re.compile(r"^([^\#]*?)\s+\#\s\[.*(not\s(linux|unix)|(?<!not\s)(osx|win)).*\]\s*$")
pat = re.compile(
r"^([^\#]*?)\s+\#\s\[.*(not\s(linux|unix)|(?<!not\s)(osx|win)).*\]\s*$"
)
# remove lines with selectors that don't apply to linux, i.e. if they contain
# "not linux", "not unix", "osx" or "win"; this also removes trailing newlines
lines = [pat.sub("", x) for x in lines]
text = "\n".join(lines)
if platform == 'linux' and ('c_stdlib_version' in text):
if platform == "linux" and ("c_stdlib_version" in text):
config = load(text, Loader=BaseLoader)
if 'c_stdlib_version' in config:
for version in config['c_stdlib_version']:
version = tuple([int(x) for x in version.split('.')])
if "c_stdlib_version" in config:
for version in config["c_stdlib_version"]:
version = tuple([int(x) for x in version.split(".")])
print(f"Found c_stdlib_version for linux: {version=}")
found_centos7 |= version == (2, 17)
@@ -105,7 +111,7 @@ def build_all(recipes_dir, arch):
raise ValueError("Neither a meta.yaml or a recipe.yaml recipes was found")
if found_cuda:
print('##vso[task.setvariable variable=NEED_CUDA;isOutput=true]1')
print("##vso[task.setvariable variable=NEED_CUDA;isOutput=true]1")
if found_centos7:
os.environ["DEFAULT_LINUX_VERSION"] = "cos7"
print("Overriding DEFAULT_LINUX_VERSION to be cos7")
@@ -118,70 +124,78 @@ def build_all(recipes_dir, arch):
if os.path.exists(cbc):
with open(cbc, "r") as f:
lines = f.readlines()
pat = re.compile(r"^([^\#]*?)\s+\#\s\[.*(not\s(osx|unix)|(?<!not\s)(linux|win)).*\]\s*$")
pat = re.compile(
r"^([^\#]*?)\s+\#\s\[.*(not\s(osx|unix)|(?<!not\s)(linux|win)).*\]\s*$"
)
# remove lines with selectors that don't apply to osx, i.e. if they contain
# "not osx", "not unix", "linux" or "win"; this also removes trailing newlines
lines = [pat.sub("", x) for x in lines]
text = "\n".join(lines)
if platform == 'osx' and (
'MACOSX_DEPLOYMENT_TARGET' in text or
'MACOSX_SDK_VERSION' in text or
'c_stdlib_version' in text):
if platform == "osx" and (
"MACOSX_DEPLOYMENT_TARGET" in text
or "MACOSX_SDK_VERSION" in text
or "c_stdlib_version" in text
):
config = load(text, Loader=BaseLoader)
if 'MACOSX_DEPLOYMENT_TARGET' in config:
for version in config['MACOSX_DEPLOYMENT_TARGET']:
version = tuple([int(x) for x in version.split('.')])
if "MACOSX_DEPLOYMENT_TARGET" in config:
for version in config["MACOSX_DEPLOYMENT_TARGET"]:
version = tuple([int(x) for x in version.split(".")])
deployment_version = max(deployment_version, version)
if 'c_stdlib_version' in config:
for version in config['c_stdlib_version']:
version = tuple([int(x) for x in version.split('.')])
if "c_stdlib_version" in config:
for version in config["c_stdlib_version"]:
version = tuple([int(x) for x in version.split(".")])
print(f"Found c_stdlib_version for osx: {version=}")
deployment_version = max(deployment_version, version)
if 'MACOSX_SDK_VERSION' in config:
for version in config['MACOSX_SDK_VERSION']:
version = tuple([int(x) for x in version.split('.')])
if "MACOSX_SDK_VERSION" in config:
for version in config["MACOSX_SDK_VERSION"]:
version = tuple([int(x) for x in version.split(".")])
sdk_version = max(sdk_version, deployment_version, version)
if 'channel_sources' not in text:
new_channel_urls = [*LOCAL_CHANNELS, 'conda-forge']
if "channel_sources" not in text:
new_channel_urls = [*LOCAL_CHANNELS, "conda-forge"]
else:
config = load(text, Loader=BaseLoader)
new_channel_urls = [*LOCAL_CHANNELS, *config['channel_sources'][0].split(',')]
new_channel_urls = [
*LOCAL_CHANNELS,
*config["channel_sources"][0].split(","),
]
if channel_urls is None:
channel_urls = new_channel_urls
elif channel_urls != new_channel_urls:
raise ValueError(f'Detected different channel_sources in the recipes: {channel_urls} vs. {new_channel_urls}. Consider submitting them in separate PRs')
raise ValueError(
f"Detected different channel_sources in the recipes: {channel_urls} vs. {new_channel_urls}. Consider submitting them in separate PRs"
)
if channel_urls is None:
channel_urls = [*LOCAL_CHANNELS, 'conda-forge']
channel_urls = [*LOCAL_CHANNELS, "conda-forge"]
with open(variant_config_file, 'r') as f:
variant_text = ''.join(f.readlines())
with open(variant_config_file, "r") as f:
variant_text = "".join(f.readlines())
if deployment_version != (0, 0):
deployment_version = '.'.join([str(x) for x in deployment_version])
deployment_version = ".".join([str(x) for x in deployment_version])
print("Overriding MACOSX_DEPLOYMENT_TARGET to be ", deployment_version)
variant_text += '\nMACOSX_DEPLOYMENT_TARGET:\n'
variant_text += "\nMACOSX_DEPLOYMENT_TARGET:\n"
variant_text += f'- "{deployment_version}"\n'
if sdk_version != (0, 0):
sdk_version = '.'.join([str(x) for x in sdk_version])
sdk_version = ".".join([str(x) for x in sdk_version])
print("Overriding MACOSX_SDK_VERSION to be ", sdk_version)
variant_text += '\nMACOSX_SDK_VERSION:\n'
variant_text += "\nMACOSX_SDK_VERSION:\n"
variant_text += f'- "{sdk_version}"\n'
with open(variant_config_file, 'w') as f:
with open(variant_config_file, "w") as f:
f.write(variant_text)
if platform == "osx" and (sdk_version != (0, 0) or deployment_version != (0, 0)):
subprocess.run("run_conda_forge_build_setup", shell=True, check=True)
if 'conda-forge' not in channel_urls:
raise ValueError('conda-forge needs to be part of channel_sources')
if "conda-forge" not in channel_urls:
raise ValueError("conda-forge needs to be part of channel_sources")
if has_meta_yaml:
print("Building {} with {}".format(','.join(folders), ','.join(channel_urls)))
print("Building {} with {}".format(",".join(folders), ",".join(channel_urls)))
build_folders(recipes_dir, folders, arch, channel_urls)
elif has_recipe_yaml:
print(
@@ -193,28 +207,31 @@ def build_all(recipes_dir, arch):
def get_config(arch, channel_urls):
exclusive_config_files = [os.path.join(conda.base.context.context.root_prefix,
'conda_build_config.yaml')]
exclusive_config_files = [
os.path.join(conda.base.context.context.root_prefix, "conda_build_config.yaml")
]
script_dir = os.path.dirname(os.path.realpath(__file__))
# since variant builds override recipe/conda_build_config.yaml, see
# https://github.com/conda/conda-build/blob/3.21.8/conda_build/variants.py#L175-L181
# we need to make sure not to use variant_configs here, otherwise
# staged-recipes PRs cannot override anything using the recipe-cbc.
exclusive_config_file = os.path.join(script_dir, '{}.yaml'.format(
get_config_name(arch)))
exclusive_config_file = os.path.join(
script_dir, "{}.yaml".format(get_config_name(arch))
)
if os.path.exists(exclusive_config_file):
exclusive_config_files.append(exclusive_config_file)
config = conda_build.api.Config(
arch=arch, exclusive_config_files=exclusive_config_files,
channel_urls=channel_urls, error_overlinking=True,
arch=arch,
exclusive_config_files=exclusive_config_files,
channel_urls=channel_urls,
error_overlinking=True,
)
return config
def build_folders(recipes_dir, folders, arch, channel_urls):
index_path = os.path.join(sys.exec_prefix, 'conda-bld')
index_path = os.path.join(sys.exec_prefix, "conda-bld")
os.makedirs(index_path, exist_ok=True)
conda_index.api.update_index(index_path)
index = conda.core.index.get_index(channel_urls=channel_urls)
@@ -223,26 +240,38 @@ def build_folders(recipes_dir, folders, arch, channel_urls):
config = get_config(arch, channel_urls)
platform = get_host_platform()
worker = {'platform': platform, 'arch': arch,
'label': '{}-{}'.format(platform, arch)}
worker = {
"platform": platform,
"arch": arch,
"label": "{}-{}".format(platform, arch),
}
G = construct_graph(recipes_dir, worker=worker, run='build',
conda_resolve=conda_resolve, folders=folders,
config=config, finalize=False)
G = construct_graph(
recipes_dir,
worker=worker,
run="build",
conda_resolve=conda_resolve,
folders=folders,
config=config,
finalize=False,
)
order = list(nx.topological_sort(G))
order.reverse()
print('Computed that there are {} distributions to build from {} recipes'
.format(len(order), len(folders)))
print(
"Computed that there are {} distributions to build from {} recipes".format(
len(order), len(folders)
)
)
if not order:
print('Nothing to do')
print("Nothing to do")
return
print("Resolved dependencies, will be built in the following order:")
print(' '+'\n '.join(order))
print(" " + "\n ".join(order))
d = OrderedDict()
for node in order:
d[G.nodes[node]['meta'].meta_path] = 1
d[G.nodes[node]["meta"].meta_path] = 1
for recipe in d.keys():
conda_build.api.build([recipe], config=get_config(arch, channel_urls))
@@ -277,15 +306,11 @@ def build_folders_rattler_build(
"--target-platform",
f"{platform}-{arch}",
]
for channel_url in channel_urls:
# Local is automatically added by rattler-build so we just remove it.
if channel_url != "local":
args.extend(["-c", channel_url])
# Construct a temporary file where we write the combined variant config. We can then pass that
# to rattler-build.
with tempfile.NamedTemporaryFile(delete=False) as fp:
fp.write(variant_config.encode("utf-8"))
fp.write(variant_config.encode("utf-8"))
atexit.register(os.unlink, fp.name)
# Execute rattler-build.
@@ -295,17 +320,21 @@ def build_folders_rattler_build(
def check_recipes_in_correct_dir(root_dir, correct_dir):
for path in Path(root_dir).glob("*"):
path = Path(path)
if path.is_dir() and path.name.lower() in ('.pixi', 'build_artifacts', 'miniforge3'):
if path.is_dir() and path.name.lower() in (
".pixi",
"build_artifacts",
"miniforge3",
):
# ignore pkg_cache in build_artifacts
continue
for recipe_path in path.rglob('*.yaml'):
for recipe_path in path.rglob("*.yaml"):
if recipe_path.name not in ("meta.yaml", "recipe.yaml"):
continue
recipe_path = recipe_path.absolute().relative_to(root_dir)
if (
(recipe_path.parts[0] != correct_dir and recipe_path.parts[0] != "broken-recipes")
or len(recipe_path.parts) != 3
):
recipe_path.parts[0] != correct_dir
and recipe_path.parts[0] != "broken-recipes"
) or len(recipe_path.parts) != 3:
raise RuntimeError(
f"recipe {recipe_path} in wrong directory; "
f"must be under {correct_dir}/<name>/"
@@ -336,21 +365,22 @@ def read_mambabuild(recipes_dir):
def use_mambabuild():
from boa.cli.mambabuild import prepare
prepare()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
'--arch',
default='64',
help='target architecture (second component of a subdir; e.g. 64, arm64, ppc64le)'
"--arch",
default="64",
help="target architecture (second component of a subdir; e.g. 64, arm64, ppc64le)",
)
args = parser.parse_args()
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
check_recipes_in_correct_dir(root_dir, "recipes")
use_mamba = read_mambabuild(os.path.join(root_dir, "recipes"))
if use_mamba:
use_mambabuild()
subprocess.run("conda clean --all --yes", shell=True, check=True)
use_mambabuild()
subprocess.run("conda clean --all --yes", shell=True, check=True)
build_all(os.path.join(root_dir, "recipes"), args.arch)
+298 -149
View File
@@ -29,6 +29,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
from __future__ import print_function, division
import logging
@@ -61,39 +62,57 @@ def freezeargs(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
args = (frozendict(arg) if isinstance(arg, dict) else arg for arg in args)
kwargs = {k: frozendict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}
kwargs = {
k: frozendict(v) if isinstance(v, dict) else v for k, v in kwargs.items()
}
return func(*args, **kwargs)
return wrapped
def package_key(metadata, worker_label, run='build'):
def package_key(metadata, worker_label, run="build"):
# get the build string from whatever conda-build makes of the configuration
used_loop_vars = metadata.get_used_loop_vars()
build_vars = '-'.join([k + '_' + str(metadata.config.variant[k]) for k in used_loop_vars
if k != 'target_platform'])
build_vars = "-".join(
[
k + "_" + str(metadata.config.variant[k])
for k in used_loop_vars
if k != "target_platform"
]
)
# kind of a special case. Target platform determines a lot of output behavior, but may not be
# explicitly listed in the recipe.
tp = metadata.config.variant.get('target_platform')
if tp and tp != metadata.config.subdir and 'target_platform' not in build_vars:
build_vars += '-target_' + tp
tp = metadata.config.variant.get("target_platform")
if tp and tp != metadata.config.subdir and "target_platform" not in build_vars:
build_vars += "-target_" + tp
key = [metadata.name(), metadata.version()]
if build_vars:
key.append(build_vars)
key.extend(['on', worker_label])
key.extend(["on", worker_label])
key = "-".join(key)
if run == 'test':
key = '-'.join(('c3itest', key))
if run == "test":
key = "-".join(("c3itest", key))
return key
def _git_changed_files(git_rev, stop_rev=None, git_root=''):
def _git_changed_files(git_rev, stop_rev=None, git_root=""):
if not git_root:
git_root = os.getcwd()
if stop_rev:
git_rev = "{0}..{1}".format(git_rev, stop_rev)
print("Changed files from:", git_rev, stop_rev, git_root)
output = subprocess.check_output(['git', '-C', git_root, 'diff-tree',
'--no-commit-id', '--name-only', '-r', git_rev])
output = subprocess.check_output(
[
"git",
"-C",
git_root,
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
git_rev,
]
)
files = output.decode().splitlines()
return files
@@ -102,8 +121,8 @@ def _get_base_folders(base_dir, changed_files):
recipe_dirs = []
for f in changed_files:
# only consider files that come from folders
if '/' in f:
f = f.split('/')[0]
if "/" in f:
f = f.split("/")[0]
try:
find_recipe(os.path.join(base_dir, f))
recipe_dirs.append(f)
@@ -112,52 +131,59 @@ def _get_base_folders(base_dir, changed_files):
return recipe_dirs
def git_changed_submodules(git_rev='HEAD@{1}', stop_rev=None, git_root='.'):
def git_changed_submodules(git_rev="HEAD@{1}", stop_rev=None, git_root="."):
if stop_rev is not None:
git_rev = "{0}..{1}".format(git_rev, stop_rev)
diff_script = pkg_resources.resource_filename('conda_concourse_ci', 'diff-script.sh')
diff_script = pkg_resources.resource_filename(
"conda_concourse_ci", "diff-script.sh"
)
diff = subprocess.check_output(['bash', diff_script, git_rev],
cwd=git_root, universal_newlines=True)
diff = subprocess.check_output(
["bash", diff_script, git_rev], cwd=git_root, universal_newlines=True
)
submodule_changed_files = [line.split() for line in diff.splitlines()]
submodules_with_recipe_changes = []
for submodule in submodule_changed_files:
for file in submodule:
if 'recipe/' in file and submodule[0] not in submodules_with_recipe_changes:
if "recipe/" in file and submodule[0] not in submodules_with_recipe_changes:
submodules_with_recipe_changes.append(submodule[0])
return submodules_with_recipe_changes
def git_new_submodules(git_rev='HEAD@{1}', stop_rev=None, git_root='.'):
def git_new_submodules(git_rev="HEAD@{1}", stop_rev=None, git_root="."):
if stop_rev is not None:
git_rev = "{0}..{1}".format(git_rev, stop_rev)
new_submodule_script = pkg_resources.resource_filename('conda_concourse_ci',
'new-submodule-script.sh')
new_submodule_script = pkg_resources.resource_filename(
"conda_concourse_ci", "new-submodule-script.sh"
)
diff = subprocess.check_output(['bash', new_submodule_script, git_rev],
cwd=git_root, universal_newlines=True)
diff = subprocess.check_output(
["bash", new_submodule_script, git_rev], cwd=git_root, universal_newlines=True
)
return diff.splitlines()
def git_renamed_folders(git_rev='HEAD@{1}', stop_rev=None, git_root='.'):
def git_renamed_folders(git_rev="HEAD@{1}", stop_rev=None, git_root="."):
if stop_rev is not None:
git_rev = "{0}..{1}".format(git_rev, stop_rev)
rename_script = pkg_resources.resource_filename('conda_concourse_ci',
'rename-script.sh')
rename_script = pkg_resources.resource_filename(
"conda_concourse_ci", "rename-script.sh"
)
renamed_files = subprocess.check_output(['bash', rename_script], cwd=git_root,
universal_newlines=True).splitlines()
renamed_files = subprocess.check_output(
["bash", rename_script], cwd=git_root, universal_newlines=True
).splitlines()
return renamed_files
def git_changed_recipes(git_rev='HEAD@{1}', stop_rev=None, git_root='.'):
def git_changed_recipes(git_rev="HEAD@{1}", stop_rev=None, git_root="."):
"""
Get the list of files changed in a git revision and return a list of
package directories that have been modified.
@@ -190,24 +216,24 @@ def _deps_to_version_dict(deps):
if len(x) == 3:
d[x[0]] = (x[1], x[2])
elif len(x) == 2:
d[x[0]] = (x[1], 'any')
d[x[0]] = (x[1], "any")
else:
d[x[0]] = ('any', 'any')
d[x[0]] = ("any", "any")
return d
def get_build_deps(meta):
build_reqs = meta.get_value('requirements/build')
build_reqs = meta.get_value("requirements/build")
if not build_reqs:
build_reqs = []
return _deps_to_version_dict(build_reqs)
def get_run_test_deps(meta):
run_reqs = meta.get_value('requirements/run')
run_reqs = meta.get_value("requirements/run")
if not run_reqs:
run_reqs = []
test_reqs = meta.get_value('test/requires')
test_reqs = meta.get_value("test/requires")
if not test_reqs:
test_reqs = []
return _deps_to_version_dict(run_reqs + test_reqs)
@@ -220,37 +246,61 @@ _rendered_recipes = {}
@lru_cache(maxsize=None)
def _get_or_render_metadata(meta_file_or_recipe_dir, worker, finalize, config=None):
global _rendered_recipes
platform = worker['platform']
arch = str(worker['arch'])
platform = worker["platform"]
arch = str(worker["arch"])
if (meta_file_or_recipe_dir, platform, arch) not in _rendered_recipes:
print("rendering {0} for {1}".format(meta_file_or_recipe_dir, worker['label']))
_rendered_recipes[(meta_file_or_recipe_dir, platform, arch)] = \
api.render(meta_file_or_recipe_dir, platform=platform, arch=arch,
verbose=False, permit_undefined_jinja=True,
bypass_env_check=True, config=config, finalize=finalize)
print("rendering {0} for {1}".format(meta_file_or_recipe_dir, worker["label"]))
_rendered_recipes[(meta_file_or_recipe_dir, platform, arch)] = api.render(
meta_file_or_recipe_dir,
platform=platform,
arch=arch,
verbose=False,
permit_undefined_jinja=True,
bypass_env_check=True,
config=config,
finalize=finalize,
)
return _rendered_recipes[(meta_file_or_recipe_dir, platform, arch)]
def add_recipe_to_graph(recipe_dir, graph, run, worker, conda_resolve,
recipes_dir=None, config=None, finalize=False):
def add_recipe_to_graph(
recipe_dir,
graph,
run,
worker,
conda_resolve,
recipes_dir=None,
config=None,
finalize=False,
):
try:
print(recipe_dir, worker, config, finalize, flush=True)
rendered = _get_or_render_metadata(recipe_dir, worker, config=config, finalize=finalize)
except (IOError, SystemExit) as e:
log.exception('invalid recipe dir: %s', recipe_dir)
rendered = _get_or_render_metadata(
recipe_dir, worker, config=config, finalize=finalize
)
except (IOError, SystemExit):
log.exception("invalid recipe dir: %s", recipe_dir)
raise
name = None
for (metadata, _, _) in rendered:
name = package_key(metadata, worker['label'], run)
for metadata, _, _ in rendered:
name = package_key(metadata, worker["label"], run)
if metadata.skip():
continue
if name not in graph.nodes():
graph.add_node(name, meta=metadata, worker=worker)
add_dependency_nodes_and_edges(name, graph, run, worker, conda_resolve, config=config,
recipes_dir=recipes_dir, finalize=finalize)
add_dependency_nodes_and_edges(
name,
graph,
run,
worker,
conda_resolve,
config=config,
recipes_dir=recipes_dir,
finalize=finalize,
)
# # add the test equivalent at the same time. This is so that expanding can find it.
# if run == 'build':
@@ -269,13 +319,15 @@ def match_peer_job(target_matchspec, other_m, this_m=None):
"""target_matchspec comes from the recipe. target_variant is the variant from the recipe whose
deps we are matching. m is the peer job, which must satisfy conda and also have matching keys
for any keys that are shared between target_variant and m.config.variant"""
match_dict = {'name': other_m.name(),
'version': other_m.version(),
'build': _fix_any(other_m.build_id(), other_m.config), }
match_dict = {
"name": other_m.name(),
"version": other_m.version(),
"build": _fix_any(other_m.build_id(), other_m.config),
}
match_record = PackageRecord(
name=match_dict['name'],
version=match_dict['version'],
build=match_dict['build'],
name=match_dict["name"],
version=match_dict["version"],
build=match_dict["build"],
build_number=int(other_m.build_number() or 0),
channel=None,
)
@@ -294,34 +346,42 @@ def add_intradependencies(graph):
"""ensure that downstream packages wait for upstream build/test (not use existing
available packages)"""
for node in graph.nodes():
if 'meta' not in graph.nodes[node]:
if "meta" not in graph.nodes[node]:
continue
# get build dependencies
m = graph.nodes[node]['meta']
m = graph.nodes[node]["meta"]
# this is pretty hard. Realistically, we would want to know
# what the build and host platforms are on the build machine.
# However, all we know right now is what machine we're actually
# on (the one calculating the graph).
test_requires = m.meta.get('test', {}).get('requires', [])
test_requires = m.meta.get("test", {}).get("requires", [])
log.info("node: {}".format(node))
log.info(" build: {}".format(m.ms_depends('build')))
log.info(" host: {}".format(m.ms_depends('host')))
log.info(" run: {}".format(m.ms_depends('run')))
log.info(" build: {}".format(m.ms_depends("build")))
log.info(" host: {}".format(m.ms_depends("host")))
log.info(" run: {}".format(m.ms_depends("run")))
log.info(" test: {}".format(test_requires))
deps = set(m.ms_depends('build') + m.ms_depends('host') + m.ms_depends('run') +
[MatchSpec(dep) for dep in test_requires or []])
deps = set(
m.ms_depends("build")
+ m.ms_depends("host")
+ m.ms_depends("run")
+ [MatchSpec(dep) for dep in test_requires or []]
)
for dep in deps:
name_matches = (n for n in graph.nodes() if graph.nodes[n]['meta'].name() == dep.name)
name_matches = (
n for n in graph.nodes() if graph.nodes[n]["meta"].name() == dep.name
)
for matching_node in name_matches:
# are any of these build dependencies also nodes in our graph?
if (match_peer_job(MatchSpec(dep),
graph.nodes[matching_node]['meta'],
m) and
(node, matching_node) not in graph.edges()):
if (
match_peer_job(
MatchSpec(dep), graph.nodes[matching_node]["meta"], m
)
and (node, matching_node) not in graph.edges()
):
# add edges if they don't already exist
graph.add_edge(node, matching_node)
@@ -336,9 +396,9 @@ def collapse_subpackage_nodes(graph):
# group nodes by their recipe path first, then within those groups by their variant
node_groups = {}
for node in graph.nodes():
if 'meta' in graph.nodes[node]:
meta = graph.nodes[node]['meta']
meta_path = meta.meta_path or meta.meta['extra']['parent_recipe']['path']
if "meta" in graph.nodes[node]:
meta = graph.nodes[node]["meta"]
meta_path = meta.meta_path or meta.meta["extra"]["parent_recipe"]["path"]
master = False
master_meta = MetaData(meta_path, config=meta.config)
@@ -347,13 +407,15 @@ def collapse_subpackage_nodes(graph):
group = node_groups.get(meta_path, {})
subgroup = group.get(deepfreeze(meta.config.variant), {})
if master:
if 'master' in subgroup:
raise ValueError("tried to set more than one node in a group as master")
subgroup['master'] = node
if "master" in subgroup:
raise ValueError(
"tried to set more than one node in a group as master"
)
subgroup["master"] = node
else:
sps = subgroup.get('subpackages', [])
sps = subgroup.get("subpackages", [])
sps.append(node)
subgroup['subpackages'] = sps
subgroup["subpackages"] = sps
group[deepfreeze(meta.config.variant)] = subgroup
node_groups[meta_path] = group
@@ -361,18 +423,19 @@ def collapse_subpackage_nodes(graph):
for variant, subgroup in group.items():
# if no node is the top-level recipe (only outputs, no top-level output), need to obtain
# package/name from recipe given by common recipe path.
subpackages = subgroup.get('subpackages')
if 'master' not in subgroup:
subpackages = subgroup.get("subpackages")
if "master" not in subgroup:
sp0 = graph.nodes[subpackages[0]]
master_meta = MetaData(recipe_path, config=sp0['meta'].config)
worker = sp0['worker']
master_key = package_key(master_meta, worker['label'])
master_meta = MetaData(recipe_path, config=sp0["meta"].config)
worker = sp0["worker"]
master_key = package_key(master_meta, worker["label"])
graph.add_node(master_key, meta=master_meta, worker=worker)
master = graph.nodes[master_key]
else:
master = subgroup['master']
master_key = package_key(graph.nodes[master]['meta'],
graph.nodes[master]['worker']['label'])
master = subgroup["master"]
master_key = package_key(
graph.nodes[master]["meta"], graph.nodes[master]["worker"]["label"]
)
# fold in dependencies for all of the other subpackages within a group. This is just
# the intersection of the edges between all nodes. Store this on the "master" node.
if subpackages:
@@ -388,16 +451,25 @@ def collapse_subpackage_nodes(graph):
graph.remove_node(subnode)
def construct_graph(recipes_dir, worker, run, conda_resolve, folders=(),
git_rev=None, stop_rev=None, matrix_base_dir=None,
config=None, finalize=False):
'''
def construct_graph(
recipes_dir,
worker,
run,
conda_resolve,
folders=(),
git_rev=None,
stop_rev=None,
matrix_base_dir=None,
config=None,
finalize=False,
):
"""
Construct a directed graph of dependencies from a directory of recipes
run: whether to use build or run/test requirements for the graph. Avoids cycles.
values: 'build' or 'test'. Actually, only 'build' matters - otherwise, it's
run/test for any other value.
'''
"""
matrix_base_dir = matrix_base_dir or recipes_dir
if not os.path.isabs(recipes_dir):
recipes_dir = os.path.normpath(os.path.join(os.getcwd(), recipes_dir))
@@ -405,25 +477,32 @@ def construct_graph(recipes_dir, worker, run, conda_resolve, folders=(),
if not folders:
if not git_rev:
git_rev = 'HEAD'
git_rev = "HEAD"
folders = git_changed_recipes(git_rev, stop_rev=stop_rev,
git_root=recipes_dir)
folders = git_changed_recipes(git_rev, stop_rev=stop_rev, git_root=recipes_dir)
graph = nx.DiGraph()
for folder in folders:
recipe_dir = os.path.join(recipes_dir, folder)
if not os.path.isdir(recipe_dir):
raise ValueError("Specified folder {} does not exist".format(recipe_dir))
add_recipe_to_graph(recipe_dir, graph, run, worker, conda_resolve,
recipes_dir, config=config, finalize=finalize)
add_recipe_to_graph(
recipe_dir,
graph,
run,
worker,
conda_resolve,
recipes_dir,
config=config,
finalize=finalize,
)
add_intradependencies(graph)
collapse_subpackage_nodes(graph)
return graph
def _fix_any(value, config):
value = re.sub('any(?:h[0-9a-f]{%d})?' % config.hash_length, '', value)
value = re.sub("any(?:h[0-9a-f]{%d})?" % config.hash_length, "", value)
return value
@@ -431,29 +510,41 @@ def _fix_any(value, config):
def _installable(name, version, build_string, config, conda_resolve):
"""Can Conda install the package we need?"""
ms = MatchSpec(
" ".join(
[name, _fix_any(version, config), _fix_any(build_string, config)]
)
" ".join([name, _fix_any(version, config), _fix_any(build_string, config)])
)
installable = conda_resolve.find_matches(ms)
if not installable:
log.warn("Dependency {name}, version {ver} is not installable from your "
"channels: {channels} with subdir {subdir}. Seeing if we can build it..."
.format(name=name, ver=version, channels=config.channel_urls,
subdir=config.host_subdir))
log.warn(
"Dependency {name}, version {ver} is not installable from your "
"channels: {channels} with subdir {subdir}. Seeing if we can build it...".format(
name=name,
ver=version,
channels=config.channel_urls,
subdir=config.host_subdir,
)
)
return installable
def _buildable(name, version, recipes_dir, worker, config, finalize):
"""Does the recipe that we have available produce the package we need?"""
possible_dirs = os.listdir(recipes_dir)
packagename_re = re.compile(r'%s(?:\-[0-9]+[\.0-9\_\-a-zA-Z]*)?$' % name)
likely_dirs = (dirname for dirname in possible_dirs if
(os.path.isdir(os.path.join(recipes_dir, dirname)) and
packagename_re.match(dirname)))
metadata_tuples = [m for path in likely_dirs
for (m, _, _) in _get_or_render_metadata(os.path.join(recipes_dir,
path), worker, finalize=finalize)]
packagename_re = re.compile(r"%s(?:\-[0-9]+[\.0-9\_\-a-zA-Z]*)?$" % name)
likely_dirs = (
dirname
for dirname in possible_dirs
if (
os.path.isdir(os.path.join(recipes_dir, dirname))
and packagename_re.match(dirname)
)
)
metadata_tuples = [
m
for path in likely_dirs
for (m, _, _) in _get_or_render_metadata(
os.path.join(recipes_dir, path), worker, finalize=finalize
)
]
# this is our target match
ms = MatchSpec(" ".join([name, _fix_any(version, config)]))
@@ -465,46 +556,82 @@ def _buildable(name, version, recipes_dir, worker, config, finalize):
return m.meta_path if available else False
def add_dependency_nodes_and_edges(node, graph, run, worker, conda_resolve, recipes_dir=None,
finalize=False, config=None):
'''add build nodes for any upstream deps that are not yet installable
def add_dependency_nodes_and_edges(
node,
graph,
run,
worker,
conda_resolve,
recipes_dir=None,
finalize=False,
config=None,
):
"""add build nodes for any upstream deps that are not yet installable
changes graph in place.
'''
metadata = graph.nodes[node]['meta']
"""
metadata = graph.nodes[node]["meta"]
# for plain test runs, ignore build reqs.
deps = get_run_test_deps(metadata)
recipes_dir = recipes_dir or os.getcwd()
# cross: need to distinguish between build_subdir (build reqs) and host_subdir
if run == 'build':
if run == "build":
deps.update(get_build_deps(metadata))
for dep, (version, build_str) in deps.items():
# we don't need worker info in _installable because it is already part of conda_resolve
if not _installable(dep, version, build_str, metadata.config, conda_resolve):
recipe_dir = _buildable(dep, version, recipes_dir, worker, metadata.config,
finalize=finalize)
recipe_dir = _buildable(
dep, version, recipes_dir, worker, metadata.config, finalize=finalize
)
if not recipe_dir:
continue
# raise ValueError("Dependency {} is not installable, and recipe (if "
# " available) can't produce desired version ({})."
# .format(dep, version))
dep_name = add_recipe_to_graph(recipe_dir, graph, 'build', worker,
conda_resolve, recipes_dir, config=config, finalize=finalize)
dep_name = add_recipe_to_graph(
recipe_dir,
graph,
"build",
worker,
conda_resolve,
recipes_dir,
config=config,
finalize=finalize,
)
if not dep_name:
raise ValueError("Tried to build recipe {0} as dependency, which is skipped "
"in meta.yaml".format(recipe_dir))
raise ValueError(
"Tried to build recipe {0} as dependency, which is skipped "
"in meta.yaml".format(recipe_dir)
)
graph.add_edge(node, dep_name)
def expand_run_upstream(graph, conda_resolve, worker, run, steps=0, max_downstream=5,
recipes_dir=None, matrix_base_dir=None):
def expand_run_upstream(
graph,
conda_resolve,
worker,
run,
steps=0,
max_downstream=5,
recipes_dir=None,
matrix_base_dir=None,
):
pass
def expand_run(graph, conda_resolve, worker, run, steps=0, max_downstream=5,
recipes_dir=None, matrix_base_dir=None, finalize=False):
def expand_run(
graph,
conda_resolve,
worker,
run,
steps=0,
max_downstream=5,
recipes_dir=None,
matrix_base_dir=None,
finalize=False,
):
"""Apply the build label to any nodes that need (re)building or testing.
"need rebuilding" means both packages that our target package depends on,
@@ -527,9 +654,16 @@ def expand_run(graph, conda_resolve, worker, run, steps=0, max_downstream=5,
for predecessor in full_graph.predecessors(node):
if max_downstream < 0 or (downstream - initial_nodes) < max_downstream:
add_recipe_to_graph(
os.path.dirname(full_graph.nodes[predecessor]['meta'].meta_path),
task_graph, run=run, worker=worker, conda_resolve=conda_resolve,
recipes_dir=recipes_dir, finalize=finalize)
os.path.dirname(
full_graph.nodes[predecessor]["meta"].meta_path
),
task_graph,
run=run,
worker=worker,
conda_resolve=conda_resolve,
recipes_dir=recipes_dir,
finalize=finalize,
)
downstream += 1
return len(graph.nodes())
@@ -539,15 +673,19 @@ def expand_run(graph, conda_resolve, worker, run, steps=0, max_downstream=5,
if steps != 0:
if not recipes_dir:
raise ValueError("recipes_dir is necessary if steps != 0. "
"Please pass it as an argument.")
raise ValueError(
"recipes_dir is necessary if steps != 0. "
"Please pass it as an argument."
)
# here we need to fully populate a graph that has the right build or run/test deps.
# We don't create this elsewhere because it is unnecessary and costly.
# get all immediate subdirectories
other_top_dirs = [d for d in os.listdir(recipes_dir)
if os.path.isdir(os.path.join(recipes_dir, d)) and
not d.startswith('.')]
other_top_dirs = [
d
for d in os.listdir(recipes_dir)
if os.path.isdir(os.path.join(recipes_dir, d)) and not d.startswith(".")
]
recipe_dirs = []
for recipe_dir in other_top_dirs:
try:
@@ -557,8 +695,14 @@ def expand_run(graph, conda_resolve, worker, run, steps=0, max_downstream=5,
pass
# constructing the graph for build will automatically also include the test deps
full_graph = construct_graph(recipes_dir, worker, 'build', folders=recipe_dirs,
matrix_base_dir=matrix_base_dir, conda_resolve=conda_resolve)
full_graph = construct_graph(
recipes_dir,
worker,
"build",
folders=recipe_dirs,
matrix_base_dir=matrix_base_dir,
conda_resolve=conda_resolve,
)
if steps >= 0:
for step in range(steps):
@@ -572,20 +716,21 @@ def expand_run(graph, conda_resolve, worker, run, steps=0, max_downstream=5,
def order_build(graph):
'''
"""
Assumes that packages are in graph.
Builds a temporary graph of relevant nodes and returns it topological sort.
Relevant nodes selected in a breadth first traversal sourced at each pkg
in packages.
'''
"""
reorder_cyclical_test_dependencies(graph)
try:
order = list(nx.topological_sort(graph))
order.reverse()
except nx.exception.NetworkXUnfeasible:
raise ValueError("Cycles detected in graph: %s", nx.find_cycle(graph,
orientation='reverse'))
raise ValueError(
"Cycles detected in graph: %s", nx.find_cycle(graph, orientation="reverse")
)
return order
@@ -608,19 +753,23 @@ def reorder_cyclical_test_dependencies(graph):
build A <-- build B <-- test A <-- test B
"""
# find all test nodes with edges to build nodes
test_nodes = [node for node in graph.nodes() if node.startswith('test-')]
edges_from_test_to_build = [edge for edge in graph.edges() if edge[0] in test_nodes and
edge[1].startswith('build-')]
test_nodes = [node for node in graph.nodes() if node.startswith("test-")]
edges_from_test_to_build = [
edge
for edge in graph.edges()
if edge[0] in test_nodes and edge[1].startswith("build-")
]
# find any of their inverses. Entries here are of the form (test-A, build-B)
circular_deps = [edge for edge in edges_from_test_to_build
if (edge[1], edge[0]) in graph.edges()]
circular_deps = [
edge for edge in edges_from_test_to_build if (edge[1], edge[0]) in graph.edges()
]
for (testA, buildB) in circular_deps:
for testA, buildB in circular_deps:
# remove build B dependence on test A
graph.remove_edge(testA, buildB)
# remove test B dependence on build B
testB = buildB.replace('build-', 'test-', 1)
testB = buildB.replace("build-", "test-", 1)
graph.remove_edge(buildB, testB)
# Add test B dependence on test A
graph.add_edge(testA, testB)
+2 -2
View File
@@ -6,13 +6,13 @@ body:
attributes:
value: |
_Please note that conda-forge team doesn't follow this repo because of the high volume of notifications._
If you would like to get the attention of the conda-forge team, please do one of the following:
1. If the issue is related to the staged-recipes infrastructure, ping the `@conda-forge/staged-recipes` team in this issue.
_Note:_ If you're not a member of the conda-forge GitHub organization, this will be disabled by GitHub and you can ask the bot to ping the team for you by entering the following command in a comment: `@conda-forge-admin, please ping conda-forge/staged-recipes`
2. If the issue is related to conda-forge, please open an issue in the [general conda-forge repo](https://github.com/conda-forge/conda-forge.github.io).
3. If you need help, join our [Zulip](https://conda-forge.zulipchat.com) community chat.
3. If you need help, join our [Zulip](https://conda-forge.zulipchat.com) community chat.
- type: textarea
id: comment
+2 -2
View File
@@ -6,13 +6,13 @@ body:
attributes:
value: |
_Please note that conda-forge team doesn't follow this repo because of the high volume of notifications._
If you would like to get the attention of the conda-forge team, please do one of the following:
1. If the issue is related to the staged-recipes infrastructure, ping the `@conda-forge/staged-recipes` team in this issue.
_Note:_ If you're not a member of the conda-forge GitHub organization, this will be disabled by GitHub and you can ask the bot to ping the team for you by entering the following command in a comment: `@conda-forge-admin, please ping conda-forge/staged-recipes`
2. If the issue is related to conda-forge, please open an issue in the [general conda-forge repo](https://github.com/conda-forge/conda-forge.github.io).
3. If you need help, join our [Zulip](https://conda-forge.zulipchat.com) community chat.
3. If you need help, join our [Zulip](https://conda-forge.zulipchat.com) community chat.
- type: textarea
id: comment
+2 -3
View File
@@ -19,7 +19,7 @@ body:
- [The conda r skeleton helpers](https://github.com/bgruening/conda_r_skeleton_helper) - to automatically generate a recipe for packages on CRAN
---
If none of these options are helpful, please fill out the following form:
- type: input
@@ -58,7 +58,7 @@ body:
For example: This package is a dependency for ...
- type: checkboxes
id: Duplicates
id: Duplicates
attributes:
label: Package is not available
description: |
@@ -76,4 +76,3 @@ body:
options:
- label: No previous issue exists and no PR has been opened.
required: true
+1 -1
View File
@@ -22,7 +22,7 @@ markComment: >
If you'd like to keep it open, please comment/push and we will be happy to oblige!
Note that very old PRs will likely need to be rebased on `main` so that they can
Note that very old PRs will likely need to be rebased on `main` so that they can
be rebuilt with the most recent CI scripts. If you have any trouble, or we missed
reviewing this PR in the first place (sorry!), feel free
to [ping the team](https://conda-forge.org/docs/maintainer/infrastructure.html#conda-forge-admin-please-ping-team)
-65
View File
@@ -1,65 +0,0 @@
name: Create feedstocks
on:
push:
branches:
- main
schedule:
- cron: '*/10 * * * *'
workflow_dispatch: null
permissions: {}
jobs:
create-feedstocks:
permissions:
contents: write # for git push
actions: read # to read runs
if: github.repository == 'conda-forge/staged-recipes'
name: Create feedstocks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
token: ${{ github.token }}
- name: Prevent multiple jobs running in parallel
id: conversion_lock
uses: beckermr/turnstyle-python@8f1ceb87dabbbbebe42257b85c368f6110bb9170 # v2
with:
abort-after-seconds: 3
poll-interval-seconds: 2
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: commit any changes upon checkout
run: |
git config --global user.email "pelson.pub+conda-forge@gmail.com"
git config --global user.name "conda-forge-admin"
git add *
git commit -am "make sure we have no windows line endings" || exit 0
for i in `seq 1 5`; do
git pull
git push
done
- name: Run feedstock creation
# outcome is evaluated before continue-on-error above
if: ${{ steps.conversion_lock.outcome == 'success' }}
run: |
# Avoid wasting CI time if there are no recipes ready for conversion
if [ "$(ls recipes/*/meta.yaml | grep -v recipes/example/meta.yaml --count)" -eq 0 ]; then
echo "No new recipes found, exiting..."
exit 0
fi
echo "Creating feedstocks from the recipe(s)."
source ./.github/workflows/scripts/create_feedstocks
env:
STAGING_BINSTAR_TOKEN: ${{ secrets.STAGING_BINSTAR_TOKEN }}
GH_TOKEN: ${{ secrets.CF_ADMIN_GITHUB_TOKEN }}
TRAVIS_TOKEN: ${{ secrets.ORGWIDE_TRAVIS_TOKEN }}
AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }}
@@ -1,58 +0,0 @@
#!/usr/bin/env bash
set -e
set -x
# Ensure we are on the latest commit
# of the branch where we are converting
# recipes from. Currently this is `main`.
export CF_CURRENT_BRANCH="${GITHUB_REF/refs\/heads\//}"
git checkout "${CF_CURRENT_BRANCH}"
# 2 core available on Travis CI Linux workers: https://docs.travis-ci.com/user/ci-environment/#Virtualization-environments
# CPU_COUNT is passed through conda build: https://github.com/conda/conda-build/pull/1149
export CPU_COUNT=2
export PYTHONUNBUFFERED=1
# Install Miniforge3.
echo ""
echo "Installing a fresh version of Miniforge3."
curl -L "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" > ~/Miniforge3.sh
chmod +x ~/Miniforge3.sh
bash ~/Miniforge3.sh -b -p ~/Miniforge3
touch ~/Miniforge3/conda-meta/pinned
(
source ~/Miniforge3/bin/activate
# Configure conda.
echo ""
echo "Configuring conda."
conda config --set show_channel_urls true
conda config --set auto_update_conda false
conda config --set add_pip_as_python_dependency false
conda config --set solver libmamba
conda update -n base --yes --quiet conda mamba conda-libmamba-solver
)
source ~/Miniforge3/bin/activate
conda install --yes --quiet \
conda-forge-ci-setup=4.* \
"conda-smithy>=3.38.0,<4.0.0a0" \
conda-forge-pinning \
"conda-build>=24.3" \
"gitpython>=3.0.8,<3.1.20" \
requests \
ruamel.yaml \
"pygithub>=2.1.1" \
"rattler-build-conda-compat>=1.2.0,<2.0.0a0" \
"conda-forge-feedstock-ops>=0.5.0" \
"conda-forge-metadata>=0.8.1"
conda info
mamba info
conda config --get
git config --global init.defaultBranch main
python .github/workflows/scripts/create_feedstocks.py
@@ -1,605 +0,0 @@
#!/usr/bin/env python
"""
Convert all recipes into feedstocks.
This script is to be run in a TravisCI context, with all secret environment
variables defined (STAGING_BINSTAR_TOKEN, GH_TOKEN)
Such as:
export GH_TOKEN=$(cat ~/.conda-smithy/github.token)
"""
from __future__ import print_function
from __future__ import annotations
import json
from pathlib import Path
from typing import Iterator
from conda_build.metadata import MetaData
from rattler_build_conda_compat.render import MetaData as RattlerBuildMetaData
from conda_smithy.utils import get_feedstock_name_from_meta
from contextlib import contextmanager
from datetime import datetime, timezone
from github import Github, GithubException
import os.path
import shutil
import subprocess
import sys
import tempfile
import traceback
import time
import github
import requests
from ruamel.yaml import YAML
from conda_forge_feedstock_ops.parse_package_and_feedstock_names import (
parse_package_and_feedstock_names
)
from conda_forge_metadata.feedstock_outputs import sharded_path as _get_sharded_path
# Enable DEBUG to run the diagnostics, without actually creating new feedstocks.
DEBUG = False
REPO_SKIP_LIST = ["core", "bot", "staged-recipes", "arm-arch", "systems", "ctx"]
recipe_directory_name = "recipes"
def _test_and_raise_besides_file_not_exists(e: github.GithubException):
if isinstance(e, github.UnknownObjectException):
return
if e.status == 404 and "No object found" in e.data["message"]:
return
raise e
def _register_package_for_feedstock(feedstock, pkg_name, gh):
repo = gh.get_repo("conda-forge/feedstock-outputs")
try:
contents = repo.get_contents(_get_sharded_path(pkg_name))
except github.GithubException as e:
_test_and_raise_besides_file_not_exists(e)
contents = None
if contents is None:
data = {"feedstocks": [feedstock]}
repo.create_file(
_get_sharded_path(pkg_name),
f"[cf admin skip] ***NO_CI*** add output {pkg_name} for conda-forge/{feedstock}-feedstock",
json.dumps(data),
)
print(f" output {pkg_name} added for feedstock {feedstock}", flush=True)
else:
# we proceed anyways and do not raise since it could be a rerun of staged recipes
# print a warning for the users
data = json.loads(contents.decoded_content.decode("utf-8"))
print(f" WARNING: output {pkg_name} already exists from feedstock(s) {data["feedstocks"]}", flush=True)
def list_recipes() -> Iterator[tuple[str, str]]:
"""
Locates all the recipes in the `recipes/` folder at the root of the repository.
For each found recipe this function returns a tuple consisting of
* the path to the recipe directory
* the name of the feedstock
"""
repository_root = Path(__file__).parent.parent.parent.parent.absolute()
repository_recipe_dir = repository_root / recipe_directory_name
# Ignore if the recipe directory does not exist.
if not repository_recipe_dir.is_dir:
return
for recipe_dir in repository_recipe_dir.iterdir():
# We don't list the "example" feedstock. It is an example, and is there
# to be helpful.
# .DS_Store is created by macOS to store custom attributes of its
# containing folder.
if recipe_dir.name in ["example", "example-v1", ".DS_Store"]:
continue
# Try to look for a conda-build recipe.
absolute_feedstock_path = repository_recipe_dir / recipe_dir
try:
yield (
str(absolute_feedstock_path),
get_feedstock_name_from_meta(MetaData(absolute_feedstock_path)),
)
continue
except OSError:
pass
# If no conda-build recipe was found, try to load a rattler-build recipe.
yield (
str(absolute_feedstock_path),
get_feedstock_name_from_meta(RattlerBuildMetaData(absolute_feedstock_path)),
)
@contextmanager
def tmp_dir(*args, **kwargs):
temp_dir = tempfile.mkdtemp(*args, **kwargs)
try:
yield temp_dir
finally:
shutil.rmtree(temp_dir)
def repo_exists(gh, organization, name):
# Use the organization provided.
org = gh.get_organization(organization)
try:
org.get_repo(name)
return True
except GithubException as e:
if e.status == 404:
return False
raise
def repo_default_branch(gh, organization, name):
# Use the organization provided.
org = gh.get_organization(organization)
try:
repo = org.get_repo(name)
return repo.default_branch
except GithubException as e:
if e.status == 404:
return "main"
raise
def _set_default_branch(feedstock_dir, default_branch):
yaml = YAML()
with open(os.path.join(feedstock_dir, "conda-forge.yml"), "r") as fp:
cfg = yaml.load(fp.read())
if "github" not in cfg:
cfg["github"] = {}
cfg["github"]["branch_name"] = default_branch
cfg["github"]["tooling_branch_name"] = "main"
if (
"upload_on_branch" in cfg
and cfg["upload_on_branch"] != default_branch
and cfg["upload_on_branch"] in ["master", "main"]
):
cfg["upload_on_branch"] = default_branch
if "conda_build" not in cfg:
cfg["conda_build"] = {}
if "error_overlinking" not in cfg["conda_build"]:
cfg["conda_build"]["error_overlinking"] = True
with open(os.path.join(feedstock_dir, "conda-forge.yml"), "w") as fp:
yaml.dump(cfg, fp)
def feedstock_token_exists(organization, name):
r = requests.get(
"https://api.github.com/repos/%s/"
"feedstock-tokens/contents/tokens/%s.json" % (organization, name),
headers={"Authorization": "token %s" % os.environ["GH_TOKEN"]},
)
if r.status_code != 200:
return False
else:
return True
def print_rate_limiting_info(gh, user):
# Compute some info about our GitHub API Rate Limit.
# Note that it doesn't count against our limit to
# get this info. So, we should be doing this regularly
# to better know when it is going to run out. Also,
# this will help us better understand where we are
# spending it and how to better optimize it.
# Get GitHub API Rate Limit usage and total
gh_api_remaining = gh.get_rate_limit().core.remaining
gh_api_total = gh.get_rate_limit().core.limit
# Compute time until GitHub API Rate Limit reset
gh_api_reset_time = gh.get_rate_limit().core.reset
gh_api_reset_time -= datetime.now(timezone.utc)
print("")
print("GitHub API Rate Limit Info:")
print("---------------------------")
print("token: ", user)
print("Currently remaining {remaining} out of {total}.".format(
remaining=gh_api_remaining, total=gh_api_total))
print("Will reset in {time}.".format(time=gh_api_reset_time))
print("")
return gh_api_remaining
def sleep_until_reset(gh):
# sleep the job with printing every minute if we are out
# of github api requests
gh_api_remaining = gh.get_rate_limit().core.remaining
if gh_api_remaining == 0:
# Compute time until GitHub API Rate Limit reset
gh_api_reset_time = gh.get_rate_limit().core.reset
gh_api_reset_time -= datetime.now(timezone.utc)
mins_to_sleep = int(gh_api_reset_time.total_seconds() / 60)
mins_to_sleep += 2
print("Sleeping until GitHub API resets.")
for i in range(mins_to_sleep):
time.sleep(60)
print("slept for minute {curr} out of {tot}.".format(
curr=i+1, tot=mins_to_sleep))
return True
else:
return False
if __name__ == '__main__':
exit_code = 0
is_merged_pr = os.environ.get('CF_CURRENT_BRANCH') == 'main'
smithy_conf = os.path.expanduser('~/.conda-smithy')
if not os.path.exists(smithy_conf):
os.mkdir(smithy_conf)
def write_token(name, token):
with open(os.path.join(smithy_conf, name + '.token'), 'w') as fh:
fh.write(token)
if 'APPVEYOR_TOKEN' in os.environ:
write_token('appveyor', os.environ['APPVEYOR_TOKEN'])
if 'CIRCLE_TOKEN' in os.environ:
write_token('circle', os.environ['CIRCLE_TOKEN'])
if 'AZURE_TOKEN' in os.environ:
write_token('azure', os.environ['AZURE_TOKEN'])
if 'DRONE_TOKEN' in os.environ:
write_token('drone', os.environ['DRONE_TOKEN'])
if 'TRAVIS_TOKEN' in os.environ:
write_token('travis', os.environ['TRAVIS_TOKEN'])
if 'STAGING_BINSTAR_TOKEN' in os.environ:
write_token('anaconda', os.environ['STAGING_BINSTAR_TOKEN'])
# gh_drone = Github(os.environ['GH_DRONE_TOKEN'])
# gh_drone_remaining = print_rate_limiting_info(gh_drone, 'GH_DRONE_TOKEN')
# gh_travis = Github(os.environ['GH_TRAVIS_TOKEN'])
gh_travis = None
gh = None
if 'GH_TOKEN' in os.environ:
write_token('github', os.environ['GH_TOKEN'])
gh = Github(os.environ['GH_TOKEN'])
# Get our initial rate limit info.
gh_remaining = print_rate_limiting_info(gh, 'GH_TOKEN')
# if we are out, exit early
# if sleep_until_reset(gh):
# sys.exit(1)
# try the other token maybe?
# if gh_remaining < gh_drone_remaining and gh_remaining < 100:
# write_token('github', os.environ['GH_DRONE_TOKEN'])
# gh = Github(os.environ['GH_DRONE_TOKEN'])
owner_info = ['--organization', 'conda-forge']
print('Calculating the recipes which need to be turned into feedstocks.')
with tmp_dir('__feedstocks') as feedstocks_dir:
feedstock_dirs = []
for recipe_dir, name in list_recipes():
if name.lower() in REPO_SKIP_LIST:
continue
if name.lower() == "ctx":
sys.exit(1)
feedstock_dir = os.path.join(feedstocks_dir, name + '-feedstock')
print('Making feedstock for {}'.format(name))
try:
subprocess.check_call(
['conda', 'smithy', 'init', recipe_dir,
'--feedstock-directory', feedstock_dir])
except subprocess.CalledProcessError:
traceback.print_exception(*sys.exc_info())
continue
if not is_merged_pr:
# We just want to check that conda-smithy is doing its
# thing without having any metadata issues.
continue
subprocess.check_call([
'git', 'remote', 'add', 'upstream_with_token',
'https://conda-forge-manager:{}@github.com/'
'conda-forge/{}-feedstock'.format(
os.environ['GH_TOKEN'],
name
)
],
cwd=feedstock_dir
)
# print_rate_limiting_info(gh_drone, 'GH_DRONE_TOKEN')
# Sometimes we already have the feedstock created. We need to
# deal with that case.
if repo_exists(gh, 'conda-forge', name + '-feedstock'):
default_branch = repo_default_branch(
gh, 'conda-forge', name + '-feedstock'
)
subprocess.check_call(
['git', 'fetch', 'upstream_with_token'], cwd=feedstock_dir)
subprocess.check_call(
['git', 'branch', '-m', default_branch, 'old'], cwd=feedstock_dir)
try:
subprocess.check_call(
[
'git', 'checkout', '-b', default_branch,
'upstream_with_token/%s' % default_branch
],
cwd=feedstock_dir)
except subprocess.CalledProcessError:
# Sometimes, we have a repo, but there are no commits on
# it! Just catch that case.
subprocess.check_call(
['git', 'checkout', '-b', default_branch], cwd=feedstock_dir)
else:
default_branch = "main"
feedstock_dirs.append([feedstock_dir, name, recipe_dir, default_branch])
# print_rate_limiting_info(gh_drone, 'GH_DRONE_TOKEN')
# set the default branch in the conda-forge.yml
_set_default_branch(feedstock_dir, default_branch)
# now register with github
subprocess.check_call(
['conda', 'smithy', 'register-github', feedstock_dir]
+ owner_info
# hack to help travis work
# + ['--extra-admin-users', gh_travis.get_user().login]
# end of hack
)
# print_rate_limiting_info(gh_drone, 'GH_DRONE_TOKEN')
if gh:
# Get our final rate limit info.
print_rate_limiting_info(gh, 'GH_TOKEN')
# drone doesn't run our jobs any more so no reason to do this
# from conda_smithy.ci_register import drone_sync
# print("Running drone sync (can take ~100s)", flush=True)
# print_rate_limiting_info(gh_drone, 'GH_DRONE_TOKEN')
# drone_sync()
# for _drone_i in range(10):
# print(
# "syncing drone - %d seconds left" % (10*(10 - _drone_i)),
# flush=True,
# )
# time.sleep(10) # actually wait
# print_rate_limiting_info(gh_drone, 'GH_DRONE_TOKEN')
# Break the previous loop to allow the TravisCI registering
# to take place only once per function call.
# Without this, intermittent failures to synch the TravisCI repos ensue.
# Hang on to any CI registration errors that occur and raise them at the end.
for num, (feedstock_dir, name, recipe_dir, default_branch) in enumerate(
feedstock_dirs
):
if name.lower() in REPO_SKIP_LIST:
continue
print("\n\nregistering CI services for %s..." % name)
if num >= 10:
exit_code = 0
break
# Try to register each feedstock with CI.
# However sometimes their APIs have issues for whatever reason.
# In order to bank our progress, we note the error and handle it.
# After going through all the recipes and removing the converted ones,
# we fail the build so that people are aware that things did not clear.
# hack to help travis work
# from conda_smithy.ci_register import add_project_to_travis
# add_project_to_travis("conda-forge", name + "-feedstock")
# print_rate_limiting_info(gh_travis, 'GH_TRAVIS_TOKEN')
# end of hack
try:
subprocess.check_call(
['conda', 'smithy', 'register-ci', '--without-appveyor',
'--without-circle', '--without-drone', '--without-cirun',
'--without-webservice', '--feedstock_directory',
feedstock_dir] + owner_info)
subprocess.check_call(
['conda', 'smithy', 'rerender', '--no-check-uptodate'], cwd=feedstock_dir)
except subprocess.CalledProcessError:
exit_code = 0
traceback.print_exception(*sys.exc_info())
continue
# slow down so we make sure we are registered
for i in range(1, 13):
time.sleep(10)
print("Waiting for registration: {i} s".format(i=i*10))
# if we get here, now we make the feedstock token and add the staging token
print("making the feedstock token and adding the staging binstar token")
try:
if not feedstock_token_exists("conda-forge", name + "-feedstock"):
subprocess.check_call(
['conda', 'smithy', 'generate-feedstock-token',
'--unique-token-per-provider',
'--feedstock_directory', feedstock_dir] + owner_info)
subprocess.check_call(
['conda', 'smithy', 'register-feedstock-token',
'--unique-token-per-provider',
'--without-circle', '--without-drone',
'--feedstock_directory', feedstock_dir] + owner_info)
# add staging token env var to all CI probiders except appveyor
# and azure
# azure has it by default and appveyor is not used
subprocess.check_call(
['conda', 'smithy', 'rotate-binstar-token',
'--without-appveyor', '--without-azure',
"--without-github-actions", '--without-circle', '--without-drone',
'--token_name', 'STAGING_BINSTAR_TOKEN'],
cwd=feedstock_dir)
yaml = YAML()
with open(os.path.join(feedstock_dir, "conda-forge.yml"), "r") as fp:
_cfg = yaml.load(fp.read())
_cfg["conda_forge_output_validation"] = True
with open(os.path.join(feedstock_dir, "conda-forge.yml"), "w") as fp:
yaml.dump(_cfg, fp)
subprocess.check_call(
["git", "add", "conda-forge.yml"],
cwd=feedstock_dir
)
subprocess.check_call(
['conda', 'smithy', 'rerender', '--no-check-uptodate'], cwd=feedstock_dir)
# pre-register outputs
print("registering outputs...")
_, pkg_names, _ = parse_package_and_feedstock_names(feedstock_dir, use_container=False)
for pkg_name in pkg_names:
_register_package_for_feedstock(name, pkg_name, gh)
except subprocess.CalledProcessError:
exit_code = 0
traceback.print_exception(*sys.exc_info())
continue
print("making a commit and pushing...")
subprocess.check_call(
['git', 'commit', '--allow-empty', '-am',
"Re-render the feedstock after CI registration."], cwd=feedstock_dir)
for i in range(5):
try:
# Capture the output, as it may contain the GH_TOKEN.
out = subprocess.check_output(
[
'git', 'push', 'upstream_with_token',
'HEAD:%s' % default_branch
],
cwd=feedstock_dir,
stderr=subprocess.STDOUT)
break
except subprocess.CalledProcessError:
pass
# Likely another job has already pushed to this repo.
# Place our changes on top of theirs and try again.
out = subprocess.check_output(
['git', 'fetch', 'upstream_with_token', default_branch],
cwd=feedstock_dir,
stderr=subprocess.STDOUT)
try:
subprocess.check_call(
[
'git', 'rebase',
'upstream_with_token/%s' % default_branch, default_branch
],
cwd=feedstock_dir)
except subprocess.CalledProcessError:
# Handle rebase failure by choosing the changes in default_branch.
subprocess.check_call(
['git', 'checkout', default_branch, '--', '.'],
cwd=feedstock_dir)
subprocess.check_call(
['git', 'rebase', '--continue'], cwd=feedstock_dir)
# Remove this recipe from the repo.
if is_merged_pr:
subprocess.check_call(['git', 'rm', '-rf', recipe_dir])
# hack to help travis work
# from conda_smithy.ci_register import travis_cleanup
# travis_cleanup("conda-forge", name + "-feedstock")
# end of hack
if gh:
# Get our final rate limit info.
print_rate_limiting_info(gh, 'GH_TOKEN')
# Update status based on the remote.
subprocess.check_call(['git', 'stash', '--keep-index', '--include-untracked'])
subprocess.check_call(['git', 'fetch'])
# CBURR: Debugging
subprocess.check_call(['git', 'status'])
subprocess.check_call(['git', 'rebase', '--autostash'])
subprocess.check_call(['git', 'add', '.'])
try:
subprocess.check_call(['git', 'stash', 'pop'])
except subprocess.CalledProcessError:
# In case there was nothing to stash.
# Finish quietly.
pass
# Parse `git status --porcelain` to handle some merge conflicts and
# generate the removed recipe list.
changed_files = subprocess.check_output(
['git', 'status', '--porcelain', recipe_directory_name],
universal_newlines=True)
changed_files = changed_files.splitlines()
# Add all files from AU conflicts. They are new files that we
# weren't tracking previously.
# Adding them resolves the conflict and doesn't actually add anything to the index.
new_file_conflicts = filter(lambda _: _.startswith("AU "), changed_files)
new_file_conflicts = map(
lambda _: _.replace("AU", "", 1).lstrip(), new_file_conflicts)
for each_new_file in new_file_conflicts:
subprocess.check_call(['git', 'add', each_new_file])
# Generate a fresh listing of recipes removed.
#
# * Each line we get back is a change to a file in the recipe directory.
# * We narrow the list down to recipes that are staged for deletion
# (ignores examples).
# * Then we clean up the list so that it only has the recipe names.
removed_recipes = filter(lambda _: _.startswith("D "), changed_files)
removed_recipes = map(lambda _: _.replace("D", "", 1).lstrip(), removed_recipes)
removed_recipes = map(
lambda _: os.path.relpath(_, recipe_directory_name), removed_recipes)
removed_recipes = map(lambda _: _.split(os.path.sep)[0], removed_recipes)
removed_recipes = sorted(set(removed_recipes))
# Commit any removed packages.
subprocess.check_call(['git', 'status'])
if removed_recipes:
msg = ('Removed recipe{s} ({}) after converting into feedstock{s}.'
''.format(', '.join(removed_recipes),
s=('s' if len(removed_recipes) > 1 else '')))
msg += ' [ci skip]'
if is_merged_pr:
# Capture the output, as it may contain the GH_TOKEN.
out = subprocess.check_output(
['git', 'remote', 'add', 'upstream_with_token',
'https://x-access-token:{}@github.com/'
'conda-forge/staged-recipes'.format(os.environ['GH_TOKEN'])],
stderr=subprocess.STDOUT)
subprocess.check_call(['git', 'commit', '-m', msg])
# Capture the output, as it may contain the GH_TOKEN.
branch = os.environ.get('CF_CURRENT_BRANCH')
out = subprocess.check_output(
['git', 'push', 'upstream_with_token', 'HEAD:%s' % branch],
stderr=subprocess.STDOUT)
else:
print('Would git commit, with the following message: \n {}'.format(msg))
if gh:
# Get our final rate limit info.
print_rate_limiting_info(gh, 'GH_TOKEN')
# if gh_drone:
# print_rate_limiting_info(gh_drone, 'GH_DRONE_TOKEN')
# if gh_travis:
# print_rate_limiting_info(gh_travis, 'GH_TRAVIS_TOKEN')
sys.exit(exit_code)
+29 -22
View File
@@ -38,10 +38,14 @@ def _lint_recipes(gh, pr):
if "maintenance" not in labels:
for fname in fnames:
if fname in example_recipes:
lints[fname].append("Do not edit or delete example recipes in `recipes/example/` or `recipe/example-v1/`.")
lints[fname].append(
"Do not edit or delete example recipes in `recipes/example/` or `recipe/example-v1/`."
)
extra_edits = True
if not fname.startswith("recipes/"):
lints[fname].append("Do not edit files outside of the `recipes/` directory.")
lints[fname].append(
"Do not edit files outside of the `recipes/` directory."
)
extra_edits = True
# 2. Make sure the new recipe is in the right directory
@@ -58,18 +62,18 @@ def _lint_recipes(gh, pr):
)
# 3. Ensure environment.yaml and pixi.toml are in sync
original_environment_yaml = (ROOT / "environment.yaml").read_text()
original_environment_yaml = (ROOT / "environment.yaml").read_text().rstrip()
pixi_exported_env_yaml = check_output(
["pixi", "project", "export", "conda-environment", "-e", "build"],
text=True,
)
).rstrip()
if original_environment_yaml != pixi_exported_env_yaml:
import difflib
_orig_lines = original_environment_yaml.splitlines(keepends=True)
_expt_lines = pixi_exported_env_yaml.splitlines(keepends=True)
print("environment diff:", flush=True)
print(''.join(difflib.unified_diff(_orig_lines, _expt_lines)), flush=True)
print("".join(difflib.unified_diff(_orig_lines, _expt_lines)), flush=True)
lints["environment.yaml"].append(
"The `environment.yaml` file is out of sync with `pixi.toml`. "
"Fix by running `pixi project export conda-environment -e build > environment.yaml`."
@@ -103,9 +107,7 @@ def _lint_recipes(gh, pr):
outputs_section = get_section(
meta, "outputs", lints, recipe_version=recipe_version
)
extra_section = get_section(
meta, "extra", lints, recipe_version=recipe_version
)
extra_section = get_section(meta, "extra", lints, recipe_version=recipe_version)
maintainers = extra_section.get("recipe-maintainers", [])
if recipe_version == 1:
@@ -138,9 +140,11 @@ def _lint_recipes(gh, pr):
feedstock_exists = False
if feedstock_exists and existing_recipe_name == recipe_name:
lints[fname].append("Feedstock with the same name exists in conda-forge.")
lints[fname].append(
"Feedstock with the same name exists in conda-forge."
)
elif feedstock_exists:
hints[fname].append(
lints[fname].append(
f"Feedstock with the name {existing_recipe_name} exists in conda-forge. "
f"Is it the same as this package ({recipe_name})?"
)
@@ -151,7 +155,7 @@ def _lint_recipes(gh, pr):
except github.GithubException as e:
_test_and_raise_besides_file_not_exists(e)
else:
hints[fname].append(
lints[fname].append(
"Recipe with the same name exists in bioconda: "
"please discuss with @conda-forge/bioconda-recipes."
)
@@ -159,7 +163,9 @@ def _lint_recipes(gh, pr):
url = None
if recipe_version == 1:
for source_section in sources_section:
if str(source_section.get("url")).startswith("https://pypi.io/packages/source/"):
if str(source_section.get("url")).startswith(
"https://pypi.io/packages/source/"
):
url = source_section["url"]
else:
for source_section in sources_section:
@@ -179,30 +185,32 @@ def _lint_recipes(gh, pr):
for pkg in mapping:
if pkg.get("pypi_name", "") == pypi_name:
conda_name = pkg["conda_name"]
hints[fname].append(
lints[fname].append(
f"A conda package with same name ({conda_name}) already exists."
)
# 5. Ensure all maintainers have commented that they approve of being listed
if maintainers:
# Get PR author, issue comments, and review comments
pr_author = pr.user.login
pr_author = pr.user.login.lower()
issue_comments = pr.get_issue_comments()
review_comments = pr.get_reviews()
# Combine commenters from both issue comments and review comments
commenters = {comment.user.login for comment in issue_comments}
commenters.update({review.user.login for review in review_comments})
commenters = {comment.user.login.lower() for comment in issue_comments}
commenters.update({review.user.login.lower() for review in review_comments})
# Check if all maintainers have either commented or are the PR author
non_participating_maintainers = set()
for maintainer in maintainers:
for orig_maintainer in maintainers:
maintainer = orig_maintainer.lower()
if (
maintainer not in commenters
and maintainer != pr_author
and maintainer not in NOCOMMENT_REQ_TEAMS
and "/" not in maintainer
):
non_participating_maintainers.add(maintainer)
non_participating_maintainers.add(orig_maintainer)
# Add a lint message if there are any non-participating maintainers
if non_participating_maintainers:
@@ -291,9 +299,9 @@ please add a `maintenance` label to the PR.\n"""
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Lint staged recipes.')
parser.add_argument('--owner', type=str, required=True, help='the repo owner')
parser.add_argument('--pr-num', type=int, required=True, help='the PR number')
parser = argparse.ArgumentParser(description="Lint staged recipes.")
parser.add_argument("--owner", type=str, required=True, help="the repo owner")
parser.add_argument("--pr-num", type=int, required=True, help="the PR number")
args = parser.parse_args()
@@ -305,4 +313,3 @@ if __name__ == "__main__":
_comment_on_pr(pr, lints, hints, extra_edits)
if lints:
sys.exit(1)
@@ -33,10 +33,16 @@ def _get_latest_run_summary(repo, workflow_run_id):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Lint staged recipes.')
parser.add_argument('--head-repo-owner', type=str, required=True, help='the head repo owner')
parser.add_argument('--workflow-run-id', type=int, required=True, help='the ID of the workflor run')
parser.add_argument('--head-sha', type=str, required=True, help='the head SHA of the PR')
parser = argparse.ArgumentParser(description="Lint staged recipes.")
parser.add_argument(
"--head-repo-owner", type=str, required=True, help="the head repo owner"
)
parser.add_argument(
"--workflow-run-id", type=int, required=True, help="the ID of the workflor run"
)
parser.add_argument(
"--head-sha", type=str, required=True, help="the head SHA of the PR"
)
args = parser.parse_args()
+1 -1
View File
@@ -5,7 +5,7 @@ for token in [
"GH_TOKEN",
"GH_TRAVIS_TOKEN",
"GH_DRONE_TOKEN",
"ORGWIDE_GH_TRAVIS_TOKEN"
"ORGWIDE_GH_TRAVIS_TOKEN",
]:
try:
gh = github.Github(os.environ[token])
+34
View File
@@ -0,0 +1,34 @@
# disable autofixing PRs, commenting "pre-commit.ci autofix" on a pull request triggers a autofix
ci:
autofix_prs: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
# standard end of line/end of file cleanup
- id: mixed-line-ending
- id: end-of-file-fixer
- id: trailing-whitespace
# ensure syntaxes are valid
- id: check-toml
- id: check-yaml
- repo: meta
# see https://pre-commit.com/#meta-hooks
hooks:
- id: check-hooks-apply
- id: check-useless-excludes
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
hooks:
# lint & attempt to correct failures (e.g. pyupgrade)
- id: ruff
args: [--fix]
# compatible replacement for black
- id: ruff-format
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.32.1
hooks:
# verify github syntaxes
- id: check-github-workflows
- id: check-dependabot
exclude: ^(broken-)?recipes/.*$
+1 -1
View File
@@ -37,7 +37,7 @@ solver: libmamba
CONDARC
# Workaround for errors related to "unsafe" directories:
# https://github.blog/2022-04-12-git-security-vulnerability-announced/#cve-2022-24765
# https://github.blog/2022-04-12-git-security-vulnerability-announced/#cve-2022-24765
git config --global --add safe.directory "${FEEDSTOCK_ROOT}"
# Copy the host recipes folder so we don't ever muck with it
+1 -1
View File
@@ -24,4 +24,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+4 -3
View File
@@ -15,9 +15,10 @@ If the issue persists, support can be found [on Zulip](https://conda-forge.zulip
## Getting started
1. Fork this repository.
2. Make a new folder in `recipes` for your package. Look at the [example recipe](recipes/example), our [documentation](http://conda-forge.org/docs/maintainer/adding_pkgs.html#) and the [FAQ](https://github.com/conda-forge/staged-recipes#faq) for help.
3. Open a pull request. Building of your package will be tested on Windows, Mac and Linux.
4. When your pull request is merged a new repository, called a feedstock, will be created in the github conda-forge organization, and build/upload of your package will automatically be triggered. Once complete, the package is available on conda-forge.
2. Make a new branch from `main` for your package's recipe.
3. Make a new folder in `recipes` for your package. Look at the [example recipe](recipes/example), our [documentation](http://conda-forge.org/docs/maintainer/adding_pkgs.html#) and the [FAQ](https://github.com/conda-forge/staged-recipes#faq) for help.
4. Open a pull request. Building of your package will be tested on Windows, Mac and Linux.
5. When your pull request is merged a new repository, called a feedstock, will be created in the github conda-forge organization, and build/upload of your package will automatically be triggered. Once complete, the package is available on conda-forge.
### `pixi`
+36 -18
View File
@@ -1,16 +1,28 @@
#!/usr/bin/env python3
#
# This file has been generated by conda-smithy in order to build the recipe
# locally.
#
import os
#!/bin/sh
"""exec" "python3" "$0" "$@" #""" # fmt: off # fmt: on
# The line above this comment is a bash / sh / zsh guard
# to stop people from running it with the wrong interpreter
import glob
import os
import platform
import subprocess
import sys
from argparse import ArgumentParser
import platform
from subprocess import check_output
def verify_system():
branch_name = check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"], text=True
).strip()
if branch_name == "main":
raise RuntimeError(
"You should run build-locally from a new branch, not 'main'. "
"Create a new one with:\n\n"
" git checkout -b your-chosen-branch-name\n"
)
BUILD_LOCALLY_FILTER = os.environ.get("BUILD_LOCALLY_FILTER", "*")
def setup_environment(ns):
os.environ["CONFIG"] = ns.config
@@ -25,13 +37,13 @@ def setup_environment(ns):
os.path.dirname(__file__), "miniforge3"
)
if "OSX_SDK_DIR" not in os.environ:
os.environ["OSX_SDK_DIR"] = os.path.join(
os.path.dirname(__file__), "SDKs"
)
os.environ["OSX_SDK_DIR"] = os.path.join(os.path.dirname(__file__), "SDKs")
# The default cache location might not be writable using docker on macOS.
if ns.config.startswith("linux") and platform.system() == "Darwin":
os.environ["CONDA_FORGE_DOCKER_RUN_ARGS"] = "-e RATTLER_CACHE_DIR=/tmp/rattler_cache"
os.environ["CONDA_FORGE_DOCKER_RUN_ARGS"] = (
"-e RATTLER_CACHE_DIR=/tmp/rattler_cache"
)
def run_docker_build(ns):
@@ -50,11 +62,13 @@ def run_win_build(ns):
def verify_config(ns):
choices_filter = ns.filter or "*"
valid_configs = {
os.path.basename(f)[:-5] for f in glob.glob(f".ci_support/{BUILD_LOCALLY_FILTER}.yaml")
os.path.basename(f)[:-5]
for f in glob.glob(f".ci_support/{choices_filter}.yaml")
}
if BUILD_LOCALLY_FILTER != "*":
print(f"filtering for '{BUILD_LOCALLY_FILTER}.yaml' configs")
if choices_filter != "*":
print(f"filtering for '{choices_filter}.yaml' configs")
print(f"valid configs are {valid_configs}")
if ns.config in valid_configs:
print("Using " + ns.config + " configuration")
@@ -88,16 +102,20 @@ def verify_config(ns):
def main(args=None):
p = ArgumentParser("build-locally")
p.add_argument("config", default=None, nargs="?")
p.add_argument(
"--filter",
default=None,
help="Glob string to filter which build choices are presented in interactive mode.",
)
p.add_argument(
"--debug",
action="store_true",
help="Setup debug environment using `conda debug`",
)
p.add_argument(
"--output-id", help="If running debug, specify the output to setup."
)
p.add_argument("--output-id", help="If running debug, specify the output to setup.")
ns = p.parse_args(args=args)
verify_system()
verify_config(ns)
setup_environment(ns)
+1 -2
View File
@@ -6,11 +6,10 @@ dependencies:
- python 3.12.*
- conda >=24.9.2
- conda-libmamba-solver >=24.9.0
- conda-build >=24.9
- conda-build >=25.3.1
- conda-index >=0.3.0
- conda-forge-ci-setup >=4.9.3,<5.0
- conda-forge-pinning *
- frozendict *
- networkx 2.4.*
- rattler-build-conda-compat >=1.2.0,<2.0.0a0
+4 -7
View File
@@ -21,7 +21,7 @@ python = "3.12.*"
[feature.build.dependencies]
conda = ">=24.9.2"
conda-libmamba-solver = ">=24.9.0"
conda-build = ">=24.9"
conda-build = ">=25.3.1"
conda-index = ">=0.3.0"
conda-forge-ci-setup = ">=4.9.3,<5.0"
conda-forge-pinning = "*"
@@ -34,17 +34,15 @@ rattler-build-conda-compat = ">=1.2.0,<2.0.0a0"
platforms = ["linux-64", "linux-aarch64", "osx-64", "osx-arm64", "win-64"]
[feature.linux.tasks.build-linux]
description = "build for Linux inside a docker container"
cmd = "python build-locally.py"
env = { BUILD_LOCALLY_FILTER = "linux*" }
cmd = "python build-locally.py --filter 'linux*'"
# The osx env will run conda-build directly on the machine.
[feature.osx]
platforms = ["osx-64", "osx-arm64"]
[feature.osx.tasks.build-osx]
description = "build directly on a MacOS host machine"
cmd = "python build-locally.py"
cmd = "python build-locally.py --filter 'osx*'"
[feature.osx.tasks.build-osx.env]
BUILD_LOCALLY_FILTER = "osx*"
CONDA_BLD_PATH = "$PIXI_PROJECT_ROOT/build_artifacts"
MINIFORGE_HOME = "$CONDA_PREFIX"
OSX_SDK_DIR = "$PIXI_PROJECT_ROOT/.pixi/macOS-SDKs"
@@ -54,9 +52,8 @@ OSX_SDK_DIR = "$PIXI_PROJECT_ROOT/.pixi/macOS-SDKs"
platforms = ["win-64"]
[feature.win.tasks.build-win]
description = "build directly on a Windows host machine"
cmd = "python build-locally.py"
cmd = "python build-locally.py --filter 'win*'"
[feature.win.tasks.build-win.env]
BUILD_LOCALLY_FILTER = "win*"
CONDA_BLD_PATH = "$PIXI_PROJECT_ROOT/build_artifacts"
MINIFORGE_HOME = "$CONDA_PREFIX"
+51
View File
@@ -0,0 +1,51 @@
{% set name = "trustpy-tools" %}
{% set version = "2.0.3" %}
package:
name: {{ name|lower }}
version: {{ version }}
source:
url: https://github.com/yaniker/TrustPy/archive/refs/tags/v{{ version }}.tar.gz
sha256: dc9e65d063110df2e52cb681d02fee1f86321ff04bf4ee57eb8fae774dadc1b4
build:
noarch: python
script: {{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
number: 0
requirements:
host:
- python {{ python_min }}
- pip
- setuptools
run:
- python >={{ python_min }}
- numpy >=1.20
- scikit-learn >=1.0
- matplotlib-base >=3.0
test:
imports:
- trustpy
requires:
- python {{ python_min }}
- pip
commands:
- pip check
about:
home: https://github.com/yaniker/trustpy
license: MIT
license_file: LICENSE
summary: 'Trustworthiness analysis and reliability metrics for predictive models.'
description: |
TrustPy is a Python library that provides a set of tools for analyzing and quantifying
trustworthiness in predictive models. It works out-the-box with the AI/ML packages and is
actively growing with the addition of new metrics and techniques.
To contribute or collaborate, please contact Erim Yanik (erimyanik@gmail.com).
extra:
recipe-maintainers:
- yaniker