From 31cce2b7ed75c109fa35909b3dfc0aa639c9e981 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Thu, 4 Jun 2026 10:17:27 -0400 Subject: Add conda_capable, conda_capable functions * Add struct CondaCapabilities --- src/lib/core/conda.c | 69 ++++++++++++++++++++++++++++++++++++++++++++ src/lib/core/include/conda.h | 39 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index 5c7779f..8548658 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -749,3 +749,72 @@ int conda_env_exists(const char *root, const char *name) { snprintf(path, sizeof(path), "%s/envs/%s", root, name); return access(path, F_OK) == 0; } + +void conda_capable_free(struct CondaCapabilities *ccap) { + guard_free(ccap->conda_version); + guard_free(ccap->mamba_version); + memset(ccap, 0, sizeof(*ccap)); +} + +int conda_capable(struct CondaCapabilities *ccap) { + struct CondaCapabilities *cc = ccap; + memset(cc, 0, sizeof(*cc)); + + if (find_program("conda")) { + cc->available = true; + } + + if (cc->available) { + int status = 0; + char *conda_version = shell_output("python3 -c 'import conda; print(conda._version.__version__)'", &status); + if (status) { + SYSERROR("conda version detection failed"); + guard_free(conda_version); + return -1; + } + if (!conda_version) { + SYSERROR("unable to allocate conda_version_raw"); + return -1; + } + strip(conda_version); + + char *mamba_version = shell_output("python3 -c 'import libmambapy; print(libmambapy.version.__version__)'", &status); + if (status) { + SYSERROR("mamba version detection failed"); + guard_free(conda_version); + guard_free(mamba_version); + return -1; + } + if (!mamba_version) { + SYSERROR("unable to allocate mamba_version"); + guard_free(conda_version); + guard_free(mamba_version); + return -1; + } + strip(mamba_version); + + cc->conda_version = strdup(conda_version); + cc->mamba_version = strdup(mamba_version); + if (version_compare(GT | EQ, cc->conda_version, "25.3.0")) { + cc->require_explicit_export_format = true; + cc->require_boa = false; + } else { + cc->require_explicit_export_format = false; + cc->require_manual_activation_shim = true; + cc->require_boa = true; + cc->require_libmamba_solver = true; + } + + struct Process proc = {0}; + safe_strncpy(proc.f_stderr, "/dev/null", sizeof(proc.f_stderr)); + if (shell(&proc, "mamba install --use-local --dry-run conda")) { + cc->missing_use_local = true; + } + + cc->usable = true; + + guard_free(mamba_version); + guard_free(conda_version); + } + return 0; +} \ No newline at end of file diff --git a/src/lib/core/include/conda.h b/src/lib/core/include/conda.h index a7108ec..66d5b57 100644 --- a/src/lib/core/include/conda.h +++ b/src/lib/core/include/conda.h @@ -26,6 +26,45 @@ #define PKG_INDEX_PROVIDES_E_MANAGER_EXEC (PKG_INDEX_PROVIDES_ERROR_MESSAGE_OFFSET + 5) #define PKG_INDEX_PROVIDES_FAILED(ECODE) ((ECODE) <= PKG_INDEX_PROVIDES_ERROR_MESSAGE_OFFSET) +/** + * @struct CondaCapabilities + * @brief Collection of feature flags to support older, newer, and transitional versions of conda/mamba. + * @see implementation in `conda_capable` + */ +struct CondaCapabilities { + /// Currently installed version of Conda + char *conda_version; + /// Currently installed version of Mamba + char *mamba_version; + /// Is conda available in the runtime environment? + bool available; + /// Can conda execute? + bool usable; + /// Do we need to pass extra arguments to "conda env export"? + bool require_explicit_export_format; + /// Do we need to inject our own shim to make Conda available? + bool require_manual_activation_shim; + /// Does this version of Conda support building with boa? + bool require_boa; + /// Does this version of Conda need to be configured to use libmamba? + bool require_libmamba_solver; + /// Does "mamba install --use-local" work on this version of mamba? + bool missing_use_local; +}; + +/** + * Check for the existence of "Conda" and set flags based on the availability of features + * @param ccap a pointer to an empty `struct CondaCapabilities` + * @return 0 on success + */ +int conda_capable(struct CondaCapabilities *ccap); + +/** + * Free memory allocated by `conda_capable` + * @param ccap a pointer to a populated `struct CondaCapabilities` + */ +void conda_capable_free(struct CondaCapabilities *ccap); + struct MicromambaInfo { char *micromamba_prefix; //!< Path to write micromamba binary char *conda_prefix; //!< Path to install conda base tree -- cgit From 03343d3d294457caa673135e738e7d1d3101e94c Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Thu, 4 Jun 2026 10:22:11 -0400 Subject: Add CondaCapabilities as argument to conda_setup_headless --- src/lib/core/conda.c | 2 +- src/lib/core/include/conda.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index 8548658..17aea02 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -561,7 +561,7 @@ int conda_check_required() { return 0; } -int conda_setup_headless() { +int conda_setup_headless(struct CondaCapabilities *cc) { if (globals.verbose) { conda_exec("config --system --set quiet false"); } else { diff --git a/src/lib/core/include/conda.h b/src/lib/core/include/conda.h index 66d5b57..f0ba1e4 100644 --- a/src/lib/core/include/conda.h +++ b/src/lib/core/include/conda.h @@ -156,7 +156,7 @@ int conda_activate(const char *root, const char *env_name); /** * Configure the active conda installation for headless operation */ -int conda_setup_headless(); +int conda_setup_headless(struct CondaCapabilities *cc); /** * Creates a Conda environment from a YAML config -- cgit From a7c5906c203d1a1356aef7bf628499ad2345f43a Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Thu, 4 Jun 2026 13:20:31 -0400 Subject: shell: throw warning if temporary script's permissions cannot be modified --- src/lib/core/system.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/lib/core') diff --git a/src/lib/core/system.c b/src/lib/core/system.c index 4f1ece6..542f9fd 100644 --- a/src/lib/core/system.c +++ b/src/lib/core/system.c @@ -37,7 +37,9 @@ int shell(struct Process *proc, char *args) { // Set the script's permissions so that only the calling user can use it // This should help prevent eavesdropping if keys are applied in plain-text // somewhere. - chmod(t_name, 0700); + if (chmod(t_name, 0700)) { + SYSWARN("unable to change script permissions: %s, %s", t_name, strerror(errno)); + } pid_t pid = fork(); if (pid == -1) { -- cgit From 6e5e7a9af59f149d1c26c0a79880cb6b89e0b674 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Thu, 4 Jun 2026 13:36:30 -0400 Subject: Add recipe_get_build_system * Rename RECIPE_TYPE_* to RECIPE_STYLE_* * Add defines for RECIPE_BUILD_* that denote which build system should be used to build the recipe --- src/lib/core/include/recipe.h | 28 ++++++++++++++++++---------- src/lib/core/recipe.c | 32 +++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 17 deletions(-) (limited to 'src/lib/core') diff --git a/src/lib/core/include/recipe.h b/src/lib/core/include/recipe.h index 4dea248..ddcbabf 100644 --- a/src/lib/core/include/recipe.h +++ b/src/lib/core/include/recipe.h @@ -6,13 +6,20 @@ #include "utils.h" //! Unable to determine recipe repo type -#define RECIPE_TYPE_UNKNOWN 0 +#define RECIPE_STYLE_UNKNOWN 0 //! Recipe repo is from conda-forge -#define RECIPE_TYPE_CONDA_FORGE 1 +#define RECIPE_STYLE_CONDA_FORGE 1 //! Recipe repo is from astroconda -#define RECIPE_TYPE_ASTROCONDA 2 +#define RECIPE_STYLE_ASTROCONDA 2 //! Recipe repo provides the required build configurations but doesn't match conda-forge or astroconda's signature -#define RECIPE_TYPE_GENERIC 3 +#define RECIPE_STYLE_GENERIC 3 + +//! Unable to determine required build system +#define RECIPE_BUILD_UNKNOWN 0 +//! Build uses meta.yaml +#define RECIPE_BUILD_CONDA_BUILD 1 +//! Build uses recipe.yaml +#define RECIPE_BUILD_RATTLER 2 /** * Download a Conda package recipe @@ -46,18 +53,18 @@ int recipe_clone(char *recipe_dir, char *url, char *gitref, char **result); * } * * int recipe_type; - * recipe_type = recipe_get_type(recipe); + * recipe_type = recipe_get_style(recipe); * switch (recipe_type) { - * case RECIPE_TYPE_CONDA_FORGE: + * case RECIPE_STYLE_CONDA_FORGE: * // do something specific for conda-forge directory structure * break; - * case RECIPE_TYPE_ASTROCONDA: + * case RECIPE_STYLE_ASTROCONDA: * // do something specific for astroconda directory structure * break; - * case RECIPE_TYPE_GENERIC: + * case RECIPE_STYLE_GENERIC: * // do something specific for a directory containing a meta.yaml config * break; - * case RECIPE_TYPE_UNKNOWN: + * case RECIPE_STYLE_UNKNOWN: * default: * // the structure is foreign or the path doesn't contain a conda recipe * break; @@ -67,6 +74,7 @@ int recipe_clone(char *recipe_dir, char *url, char *gitref, char **result); * @param repopath path to git repository containing conda recipe(s) * @return One of RECIPE_TYPE_UNKNOWN, RECIPE_TYPE_CONDA_FORGE, RECIPE_TYPE_ASTROCONDA, RECIPE_TYPE_GENERIC */ -int recipe_get_type(char *repopath); +int recipe_get_style(char *repopath); +int recipe_get_build_system(const char *repopath, int style); #endif //STASIS_RECIPE_H diff --git a/src/lib/core/recipe.c b/src/lib/core/recipe.c index 8cc8e21..2e18492 100644 --- a/src/lib/core/recipe.c +++ b/src/lib/core/recipe.c @@ -34,8 +34,26 @@ int recipe_clone(char *recipe_dir, char *url, char *gitref, char **result) { return git_clone(&proc, url, destdir, gitref); } +int recipe_get_build_system(const char *repopath, const int style) { + char filename[PATH_MAX] = {0}; + safe_strncat(filename, repopath, sizeof(filename)); -int recipe_get_type(char *repopath) { + if (style == RECIPE_STYLE_CONDA_FORGE) { + safe_strncat(filename, "/recipe/recipe.yaml", sizeof(filename)); + if (!access(filename, F_OK)) { + return RECIPE_BUILD_RATTLER; + } + return RECIPE_BUILD_CONDA_BUILD; + } + + if (style == RECIPE_STYLE_ASTROCONDA || style == RECIPE_STYLE_GENERIC) { + return RECIPE_BUILD_CONDA_BUILD; + } + + return RECIPE_BUILD_UNKNOWN; +} + +int recipe_get_style(char *repopath) { // conda-forge is a collection of repositories // "conda-forge.yml" is guaranteed to exist const char *marker[] = { @@ -44,10 +62,10 @@ int recipe_get_type(char *repopath) { "meta.yaml", NULL }; - const int type[] = { - RECIPE_TYPE_CONDA_FORGE, - RECIPE_TYPE_ASTROCONDA, - RECIPE_TYPE_GENERIC + const int style[] = { + RECIPE_STYLE_CONDA_FORGE, + RECIPE_STYLE_ASTROCONDA, + RECIPE_STYLE_GENERIC }; for (size_t i = 0; marker[i] != NULL; i++) { @@ -55,9 +73,9 @@ int recipe_get_type(char *repopath) { snprintf(path, sizeof(path), "%s/%s", repopath, marker[i]); int result = access(path, F_OK); if (!result) { - return type[i]; + return style[i]; } } - return RECIPE_TYPE_UNKNOWN; + return RECIPE_STYLE_UNKNOWN; } \ No newline at end of file -- cgit From d0f42f51818576c90a2fcce29adb54f39e952af0 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Fri, 12 Jun 2026 01:47:00 -0400 Subject: Add support for modern versions of conda and mamba * Boa is dead, but we can still use it if conda is old enough * Also because boa is dead we purge it from the list if the conda version is too new * Initial environment activation changed as well, but still compatible with older versions of conda --- src/lib/core/conda.c | 76 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 22 deletions(-) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index 17aea02..219afe2 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -326,14 +326,22 @@ int conda_exec(const char *args) { }; char conda_as[10] = {0}; + const char *last_mamba_command = NULL; safe_strncpy(conda_as, "conda", sizeof(conda_as)); for (size_t i = 0; mamba_commands[i] != NULL; i++) { if (startswith(args, mamba_commands[i])) { + last_mamba_command = mamba_commands[i]; safe_strncpy(conda_as, "mamba", sizeof(conda_as)); break; } } + const int boa_present = find_program("boa") != NULL; + if (!boa_present) { + if (last_mamba_command && !strcmp(last_mamba_command, "build")) { + safe_strncpy(conda_as, "conda", sizeof(conda_as)); + } + } const char *command_fmt = "%s %s"; const int len = snprintf(NULL, 0, command_fmt, conda_as, args); @@ -416,15 +424,12 @@ static int env0_to_runtime(const char *logfile) { int conda_activate(const char *root, const char *env_name) { const char *init_script_conda = "/etc/profile.d/conda.sh"; - const char *init_script_mamba = "/etc/profile.d/mamba.sh"; char path_conda[PATH_MAX] = {0}; - char path_mamba[PATH_MAX] = {0}; char logfile[PATH_MAX] = {0}; struct Process proc = {0}; // Where to find conda's init scripts snprintf(path_conda, sizeof(path_conda), "%s%s", root, init_script_conda); - snprintf(path_mamba, sizeof(path_mamba), "%s%s", root, init_script_mamba); // Set the path to our stdout log // Emulate mktemp()'s behavior. Give us a unique file name, but don't use @@ -448,12 +453,6 @@ int conda_activate(const char *root, const char *env_name) { return -1; } - if (access(path_mamba, F_OK) < 0) { - SYSERROR("mamba is missing: %s, %s", path_mamba, strerror(errno)); - remove(logfile); - return -1; - } - // Fully activate conda and record its effect on the runtime environment char command[PATH_MAX * 3]; const char *conda_shlvl_str = getenv("CONDA_SHLVL"); @@ -475,20 +474,19 @@ int conda_activate(const char *root, const char *env_name) { snprintf(command, sizeof(command), "set -a\n" "source %s\n" - "__conda_exe() (\n" - " \"$CONDA_PYTHON_EXE\" \"$CONDA_EXE\" $_CE_M $_CE_CONDA \"$@\"\n" - ")\n\n" - "export -f __conda_exe\n" - "source %s\n" - "__mamba_exe() (\n" - " \\local MAMBA_CONDA_EXE_BACKUP=$CONDA_EXE\n" - " \\local MAMBA_EXE=$(\\dirname \"${CONDA_EXE}\")/mamba\n" - " \"$CONDA_PYTHON_EXE\" \"$MAMBA_EXE\" $_CE_M $_CE_CONDA \"$@\"\n" - ")\n\n" - "export -f __mamba_exe\n" + "tempfile=$(mktemp)\n" + "chmod 600 \"${tempfile}\"\n" + "%s/bin/mamba shell init --shell bash --dry-run 2>/dev/null\\\n" + "| (ignore=1; \\\n" + " while read line; do \\\n" + " if [[ \"$line\" == \"#\"* ]]; then ignore=0; fi; \\\n" + " if (( ignore == 0 )); then echo $line; fi; \\\n" + " done) 1> \"${tempfile}\"\n" + "source \"${tempfile}\"\n" + "rm -f \"${tempfile}\"\n" "%s\n" "conda activate %s 1>&2\n" - "env -0\n", path_conda, path_mamba, conda_shlvl ? "conda deactivate" : ":", env_name); + "env -0\n", path_conda, root, conda_shlvl ? "conda deactivate" : ":", env_name); int retval = shell(&proc, command); if (retval) { @@ -582,6 +580,29 @@ int conda_setup_headless(struct CondaCapabilities *cc) { size_t total = 0; const char *cmd_fmt = "'%s'"; if (globals.conda_packages && strlist_count(globals.conda_packages)) { + // Push or pop build packages based on capabilities + size_t boa_index = 0; + const int boa_requested = strlist_contains(globals.conda_packages, "boa", &boa_index); + if (boa_requested && !cc->require_boa) { + SYSWARN("Removing boa from global package list due to incompatible conda version (too new): %s", cc->conda_version); + strlist_remove(globals.conda_packages, boa_index); + } else if (!boa_requested && cc->require_boa) { + SYSWARN("Adding boa to global package list"); + strlist_append(&globals.conda_packages, "boa"); + } + + if (cc->require_boa) { + if (!boa_requested) { + SYSWARN("Adding boa to global package list"); + strlist_append(&globals.conda_packages, "boa"); + } + } else { + if (boa_requested) { + SYSWARN("Removing boa from global package list due to incompatible conda version (too new): %s", cc->conda_version); + strlist_remove(globals.conda_packages, boa_index); + } + } + memset(cmd, 0, sizeof(cmd)); safe_strncpy(cmd, "install ", sizeof(cmd)); @@ -719,7 +740,18 @@ int conda_env_remove(char *name) { int conda_env_export(char *name, char *output_dir, char *output_filename) { char env_command[PATH_MAX]; - snprintf(env_command, sizeof(env_command), "env export -n %s -f %s/%s.yml", name, output_dir, output_filename); + int vr = 0; + const char *env_format = NULL; + char *version = shell_output("conda --version", &vr); + if (version) { + const size_t v_offset = strlen("conda "); + if (version_compare(GT, version + v_offset, "25.1.0")) { + env_format = "--format=yml"; + } + guard_free(version); + } + + snprintf(env_command, sizeof(env_command), "env export %s -n %s -f %s/%s.yml", env_format ? env_format : "", name, output_dir, output_filename); return conda_exec(env_command); } -- cgit From f6ddfbec1a3575c3403a896023c55bd58c8f6dc6 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Fri, 12 Jun 2026 13:46:41 -0400 Subject: Add python_importlib_metadata_version function * Use it for conda and libmamapy version detection --- src/lib/core/conda.c | 46 +++++++++++++++++++++++++++----------------- src/lib/core/include/conda.h | 18 +++++++++++++++++ 2 files changed, 46 insertions(+), 18 deletions(-) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index 219afe2..5624f59 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -194,6 +194,31 @@ int pip_exec(const char *args) { return result; } +char *python_importlib_metadata_version(const char *package_name) { + int status = 0; + char cmd[PATH_MAX] = {0}; + + if (strpbrk(package_name, "\\/*{}()|;&\"'\r\n")) { + SYSERROR("package name is invalid: '%s'", package_name); + return NULL; + } + snprintf(cmd, sizeof(cmd), "python3 -c 'from importlib.metadata import version; print(version(r\x22%s\x22))'", package_name); + + char *version = shell_output(cmd, &status); + if (status) { + SYSERROR("version detection failed"); + guard_free(version); + return NULL; + } + if (!version) { + SYSERROR("unable to allocate version"); + return NULL; + } + strip(version); + return version; +} + + static const char *PKG_ERROR_STR[] = { "success", "[internal] unhandled package manager mode", @@ -797,33 +822,18 @@ int conda_capable(struct CondaCapabilities *ccap) { } if (cc->available) { - int status = 0; - char *conda_version = shell_output("python3 -c 'import conda; print(conda._version.__version__)'", &status); - if (status) { - SYSERROR("conda version detection failed"); - guard_free(conda_version); - return -1; - } + char *conda_version = python_importlib_metadata_version("conda"); if (!conda_version) { - SYSERROR("unable to allocate conda_version_raw"); + SYSERROR("conda version detection failed"); return -1; } - strip(conda_version); - char *mamba_version = shell_output("python3 -c 'import libmambapy; print(libmambapy.version.__version__)'", &status); - if (status) { - SYSERROR("mamba version detection failed"); - guard_free(conda_version); - guard_free(mamba_version); - return -1; - } + char *mamba_version = python_importlib_metadata_version("libmambapy"); if (!mamba_version) { SYSERROR("unable to allocate mamba_version"); guard_free(conda_version); - guard_free(mamba_version); return -1; } - strip(mamba_version); cc->conda_version = strdup(conda_version); cc->mamba_version = strdup(mamba_version); diff --git a/src/lib/core/include/conda.h b/src/lib/core/include/conda.h index f0ba1e4..3e79b69 100644 --- a/src/lib/core/include/conda.h +++ b/src/lib/core/include/conda.h @@ -121,6 +121,24 @@ int python_exec(const char *args); */ int pip_exec(const char *args); +/** + * Use importlib to resolve the version of an installed package + * + * ```c + * char *numpy_version = python_importlib_metadata_version("numpy"); + * if (!numpy_version) { + * fprintf(stderr, "failed to get numpy version\n"); + * exit(1); + * } + * + * printf("numpy version: %s\n", numpy_version); + * free(numpy_version); + * ``` + * @param package_name of installed python package + * @return + */ +char *python_importlib_metadata_version(const char *package_name); + /** * Execute conda (or if possible, mamba) * Conda/Mamba is determined by PATH -- cgit From 838d2e068624bebc5461e37c1d2e7ee1f6e14c8b Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Fri, 12 Jun 2026 13:47:26 -0400 Subject: Remove redundant code left behind from development --- src/lib/core/conda.c | 11 ----------- 1 file changed, 11 deletions(-) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index 5624f59..a95b420 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -616,17 +616,6 @@ int conda_setup_headless(struct CondaCapabilities *cc) { strlist_append(&globals.conda_packages, "boa"); } - if (cc->require_boa) { - if (!boa_requested) { - SYSWARN("Adding boa to global package list"); - strlist_append(&globals.conda_packages, "boa"); - } - } else { - if (boa_requested) { - SYSWARN("Removing boa from global package list due to incompatible conda version (too new): %s", cc->conda_version); - strlist_remove(globals.conda_packages, boa_index); - } - } memset(cmd, 0, sizeof(cmd)); safe_strncpy(cmd, "install ", sizeof(cmd)); -- cgit From 4798135aec35ef84baa43dcc38a3681e490c6c78 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 16 Jun 2026 11:37:28 -0400 Subject: Store conda installation prefix in capabilities structure --- src/lib/core/conda.c | 3 ++- src/lib/core/include/conda.h | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index a95b420..b1d4cc2 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -802,7 +802,7 @@ void conda_capable_free(struct CondaCapabilities *ccap) { memset(ccap, 0, sizeof(*ccap)); } -int conda_capable(struct CondaCapabilities *ccap) { +int conda_capable(struct CondaCapabilities *ccap, const char *root) { struct CondaCapabilities *cc = ccap; memset(cc, 0, sizeof(*cc)); @@ -824,6 +824,7 @@ int conda_capable(struct CondaCapabilities *ccap) { return -1; } + cc->prefix = root; cc->conda_version = strdup(conda_version); cc->mamba_version = strdup(mamba_version); if (version_compare(GT | EQ, cc->conda_version, "25.3.0")) { diff --git a/src/lib/core/include/conda.h b/src/lib/core/include/conda.h index 3e79b69..9a6178b 100644 --- a/src/lib/core/include/conda.h +++ b/src/lib/core/include/conda.h @@ -32,6 +32,8 @@ * @see implementation in `conda_capable` */ struct CondaCapabilities { + /// Conda installation prefix + const char *prefix; /// Currently installed version of Conda char *conda_version; /// Currently installed version of Mamba @@ -55,9 +57,10 @@ struct CondaCapabilities { /** * Check for the existence of "Conda" and set flags based on the availability of features * @param ccap a pointer to an empty `struct CondaCapabilities` + * @param root conda installation root directory * @return 0 on success */ -int conda_capable(struct CondaCapabilities *ccap); +int conda_capable(struct CondaCapabilities *ccap, const char *root); /** * Free memory allocated by `conda_capable` -- cgit From 9863a96a91b853d70cc1972db1c36df65c6cfecf Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 16 Jun 2026 11:39:00 -0400 Subject: Ensure .condrc exists and conda knows where to find it --- src/lib/core/conda.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index b1d4cc2..8d2753d 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -585,6 +585,19 @@ int conda_check_required() { } int conda_setup_headless(struct CondaCapabilities *cc) { + mkdirs(cc->prefix, 0755); + + char rcpath[PATH_MAX]; + snprintf(rcpath, sizeof(rcpath), "%s/.condarc", cc->prefix); + touch(rcpath); + if (errno == ENOENT) { + errno = 0; + } + + setenv("CONDARC", rcpath, 1); + setenv("MAMBARC", rcpath, 1); + setenv("MAMBA_ROOT_PREFIX", cc->prefix, 1); + if (globals.verbose) { conda_exec("config --system --set quiet false"); } else { -- cgit From f9c7645c6491a8f38b957d52df934911e0a51472 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 16 Jun 2026 11:39:20 -0400 Subject: Ignore stderr from installation dry-run --- src/lib/core/conda.c | 1 + 1 file changed, 1 insertion(+) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index 8d2753d..1c6bd99 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -852,6 +852,7 @@ int conda_capable(struct CondaCapabilities *ccap, const char *root) { struct Process proc = {0}; safe_strncpy(proc.f_stderr, "/dev/null", sizeof(proc.f_stderr)); + safe_strncpy(proc.f_stdout, "/dev/null", sizeof(proc.f_stdout)); if (shell(&proc, "mamba install --use-local --dry-run conda")) { cc->missing_use_local = true; } -- cgit From d8154024dd0e3baac34c29cad37aaf79c03e9069 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 22 Jun 2026 00:25:32 -0400 Subject: Rewrite debug_hexdump --- src/lib/core/include/utils.h | 2 +- src/lib/core/utils.c | 78 ++++++++++++++++++-------------------------- 2 files changed, 33 insertions(+), 47 deletions(-) (limited to 'src/lib/core') diff --git a/src/lib/core/include/utils.h b/src/lib/core/include/utils.h index 3f0fe9f..e75995a 100644 --- a/src/lib/core/include/utils.h +++ b/src/lib/core/include/utils.h @@ -429,7 +429,7 @@ int gen_file_extension_str(char *filename, size_t maxlen, const char *extension) */ char *remove_extras(char *s); -void debug_hexdump(char *data, int len); +void debug_hexdump(char *data, size_t len); /** * Realloc helper diff --git a/src/lib/core/utils.c b/src/lib/core/utils.c index 31208ad..d745c51 100644 --- a/src/lib/core/utils.c +++ b/src/lib/core/utils.c @@ -946,57 +946,43 @@ int gen_file_extension_str(char *filename, const size_t maxlen, const char *exte return replace_text(ext_orig, ext_orig, extension, 0); } -#define DEBUG_HEXDUMP_FMT_BYTES 6 -#define DEBUG_HEXDUMP_ADDR_MAXLEN 20 -#define DEBUG_HEXDUMP_BYTES_MAXLEN (16 * 3 + 2) -#define DEBUG_HEXDUMP_ASCII_MAXLEN (16 + 1) -#define DEBUG_HEXDUMP_OUTPUT_MAXLEN (DEBUG_HEXDUMP_FMT_BYTES + DEBUG_HEXDUMP_ADDR_MAXLEN + DEBUG_HEXDUMP_BYTES_MAXLEN + DEBUG_HEXDUMP_ASCII_MAXLEN + 1) - -void debug_hexdump(char *data, int len) { - int count = 0; - char addr[DEBUG_HEXDUMP_ADDR_MAXLEN] = {0}; - char bytes[DEBUG_HEXDUMP_BYTES_MAXLEN] = {0}; - char ascii[DEBUG_HEXDUMP_ASCII_MAXLEN] = {0}; - char output[DEBUG_HEXDUMP_OUTPUT_MAXLEN] = {0}; +void debug_hexdump(char *data, const size_t len) { + if (!data) { + SYSWARN("DATA NULL\n"); + return; + } + if (len <= 0) { + SYSWARN("ZERO LENGTH\n"); + return; + } + const size_t window = 16; + size_t avail = len; + const size_t chunk = avail / window ? window : avail; char *start = data; - char *end = data + len; - - char *pos = start; - while (pos != end) { - if (count == 0) { - snprintf(addr + strlen(addr), sizeof(addr) - strlen(addr), "%p", pos); + while (avail > 0) { + const size_t need = chunk > avail ? avail : chunk; + char *p = start; + printf("%p | ", p); + size_t j = 0; + for (j = 0; j < need; j++) { + printf("%02x ", p[j]); } - if (count == 8) { - safe_strncat(bytes, " ", sizeof(bytes)); + const size_t padding = window - j; + for (size_t i = 0; i < padding; i++) { + printf("00 "); } - if (count > 15) { - snprintf(output, sizeof(output), "%s | %s | %s", addr, bytes, ascii); - puts(output); - memset(output, 0, sizeof(output)); - memset(addr, 0, sizeof(addr)); - memset(bytes, 0, sizeof(bytes)); - memset(ascii, 0, sizeof(ascii)); - count = 0; - continue; + printf("| "); + for (j = 0; j < need; j++) { + char *c = p + j; + printf("%c", isprint(*c) ? *c : '.'); } - - snprintf(bytes + strlen(bytes), sizeof(bytes) - strlen(bytes), "%02X ", (unsigned char) *pos); - snprintf(ascii + strlen(ascii), sizeof(ascii) - strlen(ascii), "%c", isprint(*pos) ? *pos : '.'); - - pos++; - count++; - } - - if (count <= 8) { - // Add group padding - safe_strncat(bytes, " ", sizeof(bytes)); - } - const int padding = 16 - count; - for (int i = 0; i < padding; i++) { - safe_strncat(bytes, " ", sizeof(bytes)); + for (size_t i = 0; i < padding; i++) { + printf("."); + } + printf(" |\n"); + avail -= need; + start += need; } - snprintf(output, sizeof(output), "%s | %s | %s", addr, bytes, ascii); - puts(output); } int grow(const size_t size_new, size_t *size_orig, char **data) { -- cgit From ff9bb3ce64346a479df9c7912c994f889cfbaeb9 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 22 Jun 2026 00:36:34 -0400 Subject: pkg_index_provides * Modern mamba returns zero on error, and prints its error messages to stdout. FANTASTIC. * The error detection seems to still handle the previous behavior as well with older versions. * And now we print the contents of stdout and stderr on a non-zero exit from the package manager --- src/lib/core/conda.c | 103 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 29 deletions(-) (limited to 'src/lib/core') diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index 1c6bd99..bf27d21 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -252,21 +252,30 @@ int pkg_index_provides(int mode, const char *index, const char *spec, const char SYSERROR("Unable to create log directory: %s", logdir ? logdir : "NULL"); return -1; } - const char logfile_template[] = "STASIS-package_exists.XXXXXX"; - char logfile[PATH_MAX] = {0}; - snprintf(logfile, sizeof(logfile), "%s/%s", logdir, logfile_template); - int logfd = mkstemp(logfile); - if (logfd < 0) { - SYSERROR("unable to create log file: %s", logfile); - remove(logfile); // fail harmlessly if not present - return PKG_INDEX_PROVIDES_E_INTERNAL_LOG_HANDLE; + const int stdout_stream = 0; + const int stderr_stream = 1; + const int stdout_st = 0; + //const int stderr_st = 1; + char logfile[2][PATH_MAX] = {0}; + int logfile_fd[2] = {-1, -1}; + struct stat logfile_st[2] = {0}; + + for (size_t i = 0; i < sizeof(logfile) / sizeof(logfile[0]); i++) { + const char logfile_template[] = "STASIS-package_exists.XXXXXX"; + snprintf(logfile[i], sizeof(logfile[i]), "%s/%s", logdir, logfile_template); + logfile_fd[i] = mkstemp(logfile[i]); + if (logfile_fd[i] < 0) { + SYSERROR("unable to create log file: %s", logfile[i]); + remove(logfile[i]); // fail harmlessly if not present + return PKG_INDEX_PROVIDES_E_INTERNAL_LOG_HANDLE; + } } int status = 0; struct Process proc = {0}; - proc.redirect_stderr = 1; - snprintf(proc.f_stdout, sizeof(proc.f_stdout), "%s", logfile); + snprintf(proc.f_stdout, sizeof(proc.f_stdout), "%s", logfile[stdout_stream]); + snprintf(proc.f_stderr, sizeof(proc.f_stderr), "%s", logfile[stderr_stream]); if (mode == PKG_USE_PIP) { // Do an installation in dry-run mode to see if the package exists in the given index. @@ -293,26 +302,40 @@ int pkg_index_provides(int mode, const char *index, const char *spec, const char SYSDEBUG("Executing: %s", cmd); status = shell(&proc, cmd); - SYSDEBUG("Log file: %s", logfile); - if (status != 0) { - FILE *fp = fdopen(logfd, "r"); - if (!fp) { - remove(logfile); + // Populate stat data for log files + for (size_t i = 0; i < sizeof(logfile) / sizeof(logfile[0]); i++) { + if (stat(logfile[i], &logfile_st[i])) { + SYSERROR("Unable to stat %s", logfile[i]); return -1; } + } + + if (status != 0) { + SYSERROR("Command exited non-zero (%d)", status); + for (size_t i = 0; i < sizeof(logfile) / sizeof(logfile[0]); i++) { + const char *stream_name = i == 0 ? "stdout" : "stderr"; + if (!logfile_st[i].st_size) { + continue; + } + SYSDEBUG("(%s): %s", stream_name, logfile[i]); + FILE *fp = fdopen(logfile_fd[i], "r"); + if (!fp) { + remove(logfile[i]); + return -1; + } - fflush(stdout); - fflush(stderr); + fflush(stdout); + fflush(stderr); - char line[STASIS_BUFSIZ] = {0}; - while (fgets(line, sizeof(line) - 1, fp) != NULL) { - SYSDEBUG("%s", strip(line)); - } + char line[STASIS_BUFSIZ] = {0}; + while (fgets(line, sizeof(line) - 1, fp) != NULL) { + SYSINFO("(%s): %s", stream_name, strip(line)); + } - fflush(stderr); - fclose(fp); + fflush(stderr); + fclose(fp); + } } - remove(logfile); if (WTERMSIG(proc.returncode)) { // This gets its own return value because if the external program @@ -320,18 +343,40 @@ int pkg_index_provides(int mode, const char *index, const char *spec, const char return PKG_INDEX_PROVIDES_E_MANAGER_SIGNALED; } + int final = PKG_FOUND; if (status < 0) { - return PKG_INDEX_PROVIDES_E_MANAGER_EXEC; + final = PKG_INDEX_PROVIDES_E_MANAGER_EXEC; } else if (WEXITSTATUS(proc.returncode) > 1) { // Pip and conda both return 2 on argument parsing errors - return PKG_INDEX_PROVIDES_E_MANAGER_RUNTIME; + final = PKG_INDEX_PROVIDES_E_MANAGER_RUNTIME; + } else if (logfile_st[stdout_st].st_size > 1 && WEXITSTATUS(proc.returncode) == 0) { + // modern mamba return zero when a package not found. + // even more ridiculous; the error messages are written to stdout, not stderr + struct StrList *output = strlist_init(); + if (!output) { + SYSERROR("unable to allocate memory for stdout log list"); + return -1; + } + if (strlist_append_file(output, logfile[stdout_stream], NULL)) { + strlist_free(&output); + SYSERROR("unable to append stdout log to list: %s", logfile[stdout_stream]); + return -1; + } + if (strlist_contains(output, "No entries", NULL)) { + final = PKG_NOT_FOUND; + } + strlist_free(&output); } else if (WEXITSTATUS(proc.returncode) == 1) { // Pip and conda both return 1 when a package is not found. // Unfortunately this applies to botched version specs, too. - return PKG_NOT_FOUND; - } else { - return PKG_FOUND; + final = PKG_NOT_FOUND; + } + + // Remove log files + for (size_t i = 0; i < sizeof(logfile) / sizeof(logfile[0]); i++) { + remove(logfile[stdout_stream]); } + return final; } int conda_exec(const char *args) { -- cgit From 2fbd9e5cf2bd5d2bfe635007a0c458a58de2856e Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 23 Jun 2026 10:36:20 -0400 Subject: Fix zstd file magic --- src/lib/core/utils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/lib/core') diff --git a/src/lib/core/utils.c b/src/lib/core/utils.c index d745c51..462604d 100644 --- a/src/lib/core/utils.c +++ b/src/lib/core/utils.c @@ -1281,7 +1281,7 @@ int is_file_compressed(const char *filename) { {(unsigned char *) "PK\03\04", 3}, // zip {(unsigned char *) "PK\05\06", 3}, // zip (empty) {(unsigned char *) "PK\07\08", 3}, // zip (spanned) - {(unsigned char *) "\xfd\x2f\xb5\x28", 4} // zstd + {(unsigned char *) "\x28\xb5\x2f\xfd", 4} // zstd }; unsigned char buf[8] = {0}; // unsigned long size_t bytes_read = 0; -- cgit