diff options
| author | Joseph Hunkeler <jhunkeler@users.noreply.github.com> | 2026-06-18 14:05:46 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-18 14:05:46 -0400 |
| commit | e2008513b5fb4ae71d87ca6d05bdab5f3cb3a53f (patch) | |
| tree | 46faf70adbcbe727f6f74f07f387f8330ee390ab | |
| parent | 252b9646c1cb0538123d51ced4a733f3dcfc266b (diff) | |
| download | stasis-19b84ad71d07d5126a3fadf80ad3ac01199290ac.tar.gz | |
* Update micromamba installation logic
* Split installation from micromamba() into micromamba_install()
* Return -1 when micromamba cannot be installed
* Add stasis.ini option "indexer.micromamba_download_url"
* Add global variable micromamba_download_url
* The installation function attempts two known-good URLs by default
*
* Add indexer arugment '--micromamba-download-url'
* Add is_file_compressed() function to utils
* Call micromamba_install() from tests
* Add space in usage statement
* Fix usage output when option array contains arguments without short options
* Add --micromamba-download-url to README.md
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | src/cli/stasis_indexer/args.c | 18 | ||||
| -rw-r--r-- | src/cli/stasis_indexer/helpers.c | 6 | ||||
| -rw-r--r-- | src/cli/stasis_indexer/include/args.h | 2 | ||||
| -rw-r--r-- | src/cli/stasis_indexer/stasis_indexer_main.c | 31 | ||||
| -rw-r--r-- | src/lib/core/conda.c | 96 | ||||
| -rw-r--r-- | src/lib/core/globals.c | 1 | ||||
| -rw-r--r-- | src/lib/core/include/conda.h | 9 | ||||
| -rw-r--r-- | src/lib/core/include/core.h | 1 | ||||
| -rw-r--r-- | src/lib/core/include/utils.h | 18 | ||||
| -rw-r--r-- | src/lib/core/utils.c | 46 | ||||
| -rw-r--r-- | src/lib/delivery/delivery_populate.c | 5 | ||||
| -rw-r--r-- | stasis.ini | 9 | ||||
| -rw-r--r-- | tests/test_conda.c | 1 |
14 files changed, 227 insertions, 31 deletions
@@ -177,13 +177,14 @@ stasis mydelivery.ini ## Indexer Command Line Options -| Long Option | Short Option | Purpose | -|:-------------|:------------:|:----------------------------------------| -| --help | -h | Display this usage statement | -| --destdir | -d | Destination directory | -| --verbose | -v | Increase output verbosity | -| --unbuffered | -U | Disable line buffering | -| --web | -w | Generate HTML indexes (requires pandoc) | +| Long Option | Short Option | Purpose | +|:--------------------------|:------------:|:----------------------------------------| +| --help | -h | Display this usage statement | +| --destdir | -d | Destination directory | +| --verbose | -v | Increase output verbosity | +| --unbuffered | -U | Disable line buffering | +| --web | -w | Generate HTML indexes (requires pandoc) | +| --micromamba-download-url | n/a | Set micromamba download URL | ## Environment variables diff --git a/src/cli/stasis_indexer/args.c b/src/cli/stasis_indexer/args.c index 0d0e9b9..440c671 100644 --- a/src/cli/stasis_indexer/args.c +++ b/src/cli/stasis_indexer/args.c @@ -7,6 +7,7 @@ struct option long_options[] = { {"verbose", no_argument, 0, 'v'}, {"unbuffered", no_argument, 0, 'U'}, {"web", no_argument, 0, 'w'}, + {"micromamba-download-url", required_argument, 0, OPT_MICROMAMBA_DOWNLOAD_URL}, {0, 0, 0, 0}, }; @@ -16,6 +17,7 @@ const char *long_options_help[] = { "Increase output verbosity", "Disable line buffering", "Generate HTML indexes (requires pandoc)", + "Set micromamba download URL", NULL, }; @@ -26,17 +28,23 @@ void usage(char *name) { SYSERROR("Unable to allocate memory for options array"); exit(1); } - for (int i = 0; i < maxopts; i++) { - opts[i] = (char) long_options[i].val; + for (int i = 0, n = 0; i < maxopts; i++) { + if (isalnum(long_options[i].val)) { + opts[n] = (char) long_options[i].val; + n++; + } } - printf("usage: %s [-%s] {{STASIS_ROOT}...}\n", name, opts); + printf("usage: %s [-%s] {{STASIS_ROOT} ...}\n", name, opts); guard_free(opts); for (int i = 0; i < maxopts - 1; i++) { char line[255] = {0}; - snprintf(line, sizeof(line), " --%s -%c %-20s", long_options[i].name, long_options[i].val, long_options_help[i]); + snprintf(line, sizeof(line), " --%s %s%c %s", + long_options[i].name, + isalnum(long_options[i].val) ? "-" : "", + isalnum(long_options[i].val) ? long_options[i].val : ' ', + long_options_help[i]); puts(line); } } - diff --git a/src/cli/stasis_indexer/helpers.c b/src/cli/stasis_indexer/helpers.c index 3ef96e4..b89622b 100644 --- a/src/cli/stasis_indexer/helpers.c +++ b/src/cli/stasis_indexer/helpers.c @@ -157,6 +157,7 @@ int micromamba_configure(const struct Delivery *ctx, struct MicromambaInfo *m) { m->conda_prefix = globals.conda_install_prefix; m->micromamba_prefix = micromamba_prefix; m->download_dir = ctx->storage.tmpdir; + m->download_url = globals.micromamba_download_url; const size_t pathvar_len = strlen(getenv("PATH")) + strlen(m->micromamba_prefix) + strlen(m->conda_prefix) + 3 + 4 + 1; // ^^^^^^^^^^^^^^^^^^ @@ -172,6 +173,11 @@ int micromamba_configure(const struct Delivery *ctx, struct MicromambaInfo *m) { setenv("PATH", pathvar, 1); guard_free(pathvar); + if (micromamba_install(m)) { + SYSERROR("Micromamba installation failed"); + return -1; + } + status += micromamba(m, "config prepend --env channels conda-forge"); if (!globals.verbose) { status += micromamba(m, "config set --env quiet true"); diff --git a/src/cli/stasis_indexer/include/args.h b/src/cli/stasis_indexer/include/args.h index 543aa4b..080863c 100644 --- a/src/cli/stasis_indexer/include/args.h +++ b/src/cli/stasis_indexer/include/args.h @@ -3,6 +3,8 @@ #include <getopt.h> +#define OPT_MICROMAMBA_DOWNLOAD_URL 1000 + extern struct option long_options[]; void usage(char *name); diff --git a/src/cli/stasis_indexer/stasis_indexer_main.c b/src/cli/stasis_indexer/stasis_indexer_main.c index 45bbb6c..5d86f29 100644 --- a/src/cli/stasis_indexer/stasis_indexer_main.c +++ b/src/cli/stasis_indexer/stasis_indexer_main.c @@ -197,6 +197,13 @@ int main(const int argc, char *argv[]) { case 'w': do_html = 1; break; + case OPT_MICROMAMBA_DOWNLOAD_URL: + globals.micromamba_download_url = strdup(optarg); + if (!globals.micromamba_download_url) { + SYSERROR("unable to allocate memory for micromamba_download_url"); + exit(1); + } + break; case '?': default: exit(1); @@ -309,6 +316,30 @@ int main(const int argc, char *argv[]) { printf(BANNER, version, AUTHOR); guard_free(version); + char *cfg_path = NULL; + if (asprintf(&cfg_path, "%s/stasis.ini", globals.sysconfdir) < 0) { + SYSERROR("Unable to allocate memory for cfg path"); + exit(1); + } + ctx._stasis_ini_fp.cfg_path = cfg_path; + ctx._stasis_ini_fp.cfg = ini_open(ctx._stasis_ini_fp.cfg_path); + if (ctx._stasis_ini_fp.cfg) { + if (populate_delivery_cfg(&ctx, INI_READ_RENDER)) { + SYSERROR("Unable to apply stasis configuration"); + exit(1); + } + } + else { + SYSWARN("Unable to open stasis configuration file: %s", cfg_path); + } + + if (globals.micromamba_download_url && isempty(globals.micromamba_download_url)) { + // safeguard against supplying a zero-length URL + // this covers the case where the user supplied it as an argument and/or in the config file + SYSERROR("micromamba download URL cannot be empty"); + exit(1); + } + indexer_init_dirs(&ctx, workdir); msg(STASIS_MSG_L1, "%s delivery root %s\n", diff --git a/src/lib/core/conda.c b/src/lib/core/conda.c index c9eb750..5c7779f 100644 --- a/src/lib/core/conda.c +++ b/src/lib/core/conda.c @@ -5,7 +5,11 @@ #include "conda.h" #include "version_compare.h" -int micromamba(const struct MicromambaInfo *info, char *command, ...) { +int micromamba_install(const struct MicromambaInfo *info) { + if (access(info->micromamba_prefix, F_OK) == 0) { + SYSINFO("micromamba already installed: %s", info->micromamba_prefix); + return 0; + } struct utsname sys; uname(&sys); @@ -28,37 +32,91 @@ int micromamba(const struct MicromambaInfo *info, char *command, ...) { return -1; } - char url[PATH_MAX] = {0}; - snprintf(url, sizeof(url), "https://micro.mamba.pm/api/micromamba/%s-%s/latest", sys.sysname, sys.machine); + // If we ever want an exact version instead of "latest", we need to use this format instead: + // https://github.com/mamba-org/micromamba-releases/releases/download/${version}/micromamba-${arch} + + // Micromamba hosts binaries on github and on their own website. Prefer github. + // The "latest" binary from micromamba's site is compressed with bzip2 (06/2026) + const char *url_fmts[] = { + info->download_url, + "https://github.com/mamba-org/micromamba-releases/releases/latest/download/micromamba-%s-%s.tar.bz2", + "https://micro.mamba.pm/api/micromamba/%s-%s/latest", + }; + const size_t url_fmts_max = sizeof(url_fmts) / sizeof(url_fmts[0]); - const char installer_name[] = "mm_latest"; + char url[PATH_MAX] = {0}; char installer_path[PATH_MAX] = {0}; - snprintf(installer_path, sizeof(installer_path), "%s/%s", info->download_dir, installer_name); + const char *installer_name = "mm_latest"; + + size_t fail_count = 0; + for (size_t url_i = 0; url_i < url_fmts_max; url_i++) { + if (url_i == 0 && info->download_url) { + // If the caller supplies a download_url, use it + snprintf(url, sizeof(url), "%s", info->download_url); + } else if (url_i == 0 && isempty(info->download_url)) { + // No download_url has been defined, or is an empty string + continue; + } else { + snprintf(url, sizeof(url), url_fmts[url_i], sys.sysname, sys.machine); + } - if (access(installer_path, F_OK)) { - char *errmsg = NULL; - const long http_code = download(url, installer_path, &errmsg); - if (HTTP_ERROR(http_code)) { - SYSERROR("download failed: %ld: %s", http_code, errmsg); - guard_free(errmsg); - return -1; + snprintf(installer_path, sizeof(installer_path), "%s/%s", info->download_dir, installer_name); + + if (access(installer_path, F_OK)) { + char *errmsg = NULL; + const long http_code = download(url, installer_path, &errmsg); + if (HTTP_ERROR(http_code)) { + SYSERROR("download failed: %ld: %s", http_code, errmsg); + guard_free(errmsg); + remove(installer_path); + fail_count++; + continue; + } + break; } } + if (fail_count >= url_fmts_max) { + SYSERROR("all download attempts failed"); + return -1; + } + char mmbin[PATH_MAX]; snprintf(mmbin, sizeof(mmbin), "%s/micromamba", info->micromamba_prefix); if (access(mmbin, F_OK)) { - char untarcmd[PATH_MAX * 2]; mkdirs(info->micromamba_prefix, 0755); - snprintf(untarcmd, sizeof(untarcmd), - "tar -xvf %s -C %s --strip-components=1 bin/micromamba 1>/dev/null", - installer_path, info->micromamba_prefix); - int untarcmd_status = system(untarcmd); - if (untarcmd_status) { - return -1; + + if (is_file_compressed(installer_path)) { + char untarcmd[PATH_MAX * 2] = {0}; + snprintf(untarcmd, sizeof(untarcmd), + "tar -xvf %s -C %s --strip-components=1 bin/micromamba 1>/dev/null", + installer_path, info->micromamba_prefix); + int untarcmd_status = system(untarcmd); + if (untarcmd_status) { + return -1; + } + } else { + if (copy2(installer_path, mmbin, CT_PERM)) { + SYSERROR("unable to copy %s to %s", installer_path, mmbin); + return -1; + } + if (chmod(mmbin, 0755)) { + SYSERROR("unable to set permissions: %s (%s)", mmbin, strerror(errno)); + return -1; + } } } + return 0; +} + +int micromamba(const struct MicromambaInfo *info, char *command, ...) { + char mmbin[PATH_MAX] = {0}; + snprintf(mmbin, sizeof(mmbin), "%s/micromamba", info->micromamba_prefix); + if (access(mmbin, F_OK) < 0) { + SYSERROR("Unable to run micromamba. Not installed?"); + return -1; + } char cmd[STASIS_BUFSIZ] = {0}; va_list args; diff --git a/src/lib/core/globals.c b/src/lib/core/globals.c index b84213e..7eb1beb 100644 --- a/src/lib/core/globals.c +++ b/src/lib/core/globals.c @@ -29,6 +29,7 @@ struct STASIS_GLOBAL globals = { .verbose = false, ///< Toggle verbose mode .continue_on_error = false, ///< Do not stop program on error .always_update_base_environment = false, ///< Run "conda update --all" after installing Conda + .micromamba_download_url = NULL, .conda_fresh_start = true, ///< Remove/reinstall Conda at startup .conda_install_prefix = NULL, ///< Path to install Conda .conda_packages = NULL, ///< Conda packages to install diff --git a/src/lib/core/include/conda.h b/src/lib/core/include/conda.h index cc9426d..a7108ec 100644 --- a/src/lib/core/include/conda.h +++ b/src/lib/core/include/conda.h @@ -30,9 +30,18 @@ struct MicromambaInfo { char *micromamba_prefix; //!< Path to write micromamba binary char *conda_prefix; //!< Path to install conda base tree char *download_dir; //!< Path to store micromamba installer + char *download_url; //!< Raw URL to a remotely hosted micromamba-platform-arch file }; /** + * Download and install Micromamba + * + * @param info MicromambaInfo data structure (must be populated before use) + * @return 0 on successful installation + */ +int micromamba_install(const struct MicromambaInfo *info); + +/** * Execute micromamba * @param info MicromambaInfo data structure (must be populated before use) * @param command printf-style formatter string diff --git a/src/lib/core/include/core.h b/src/lib/core/include/core.h index 9a2007c..ed523be 100644 --- a/src/lib/core/include/core.h +++ b/src/lib/core/include/core.h @@ -35,6 +35,7 @@ struct STASIS_GLOBAL { bool verbose; //!< Enable verbose output bool always_update_base_environment; //!< Update base environment immediately after activation bool continue_on_error; //!< Do not stop on test failures + char *micromamba_download_url; //!< Override download URL for micromamba bool conda_fresh_start; //!< Always install a new copy of Conda bool enable_docker; //!< Enable docker image builds bool enable_artifactory; //!< Enable artifactory uploads diff --git a/src/lib/core/include/utils.h b/src/lib/core/include/utils.h index 98b8ae8..3f0fe9f 100644 --- a/src/lib/core/include/utils.h +++ b/src/lib/core/include/utils.h @@ -492,4 +492,22 @@ int get_random_bytes(char *result, size_t maxlen); int non_format_len(const char *s); char *center_text(const char *s, size_t maxwidth); + +/** + * Check magic bytes against known compression formats + * + * ```c + * const char *filename = "/path/to/file.zip"; + * if (is_file_compressed(filename)) { + * // file is compressed + * } else { + * // file is not compressed + * } + * ``` + * + * @param filename path to maybe-compressed file + * @return 0 if not compressed + * @return 1 if compressed + */ +int is_file_compressed(const char *filename); #endif //STASIS_UTILS_H diff --git a/src/lib/core/utils.c b/src/lib/core/utils.c index b4a520d..152c5c5 100644 --- a/src/lib/core/utils.c +++ b/src/lib/core/utils.c @@ -1273,3 +1273,49 @@ char *center_text(const char *s, const size_t maxwidth) { return result; } +int is_file_compressed(const char *filename) { + FILE *fp = fopen(filename, "rb"); + if (!fp) { + SYSERROR("Unable to open for reading: %s", filename); + return -1; + } + + // Container for magic bytes + struct Magic { + const unsigned char *byte; + const size_t size; + }; + + // Array of magic bytes for different compression types + const struct Magic magic[] = { + {(unsigned char *) "BZh", 3}, // bzip2 + {(unsigned char *) "\x1f\x8b", 2}, // gzip + {(unsigned char *) "\xfd\x37\x7a\x58\x5a\x00", 6}, // xz + {(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 buf[8] = {0}; // unsigned long + size_t bytes_read = 0; + bytes_read = fread(buf, 1, sizeof(buf), fp); + if (bytes_read < sizeof(buf)) { + SYSWARN("consumed fewer than %zu bytes (%zu) from %s", sizeof(buf), bytes_read, filename); + fclose(fp); + // well, the file isn't compressed. + return 0; + } + fclose(fp); + + // Compare known magic bytes to consumed data + for (size_t i = 0; i < sizeof(magic) / sizeof(magic[0]); i++) { + if (memcmp(buf, magic[i].byte, magic[i].size) == 0) { + // match + return 1; + } + } + + // no match + return 0; +} + diff --git a/src/lib/delivery/delivery_populate.c b/src/lib/delivery/delivery_populate.c index cfa3da2..5ce11d7 100644 --- a/src/lib/delivery/delivery_populate.c +++ b/src/lib/delivery/delivery_populate.c @@ -99,6 +99,11 @@ int populate_delivery_cfg(struct Delivery *ctx, int render_mode) { } err = 0; + if (!globals.micromamba_download_url) { + globals.micromamba_download_url = ini_getval_str(cfg, "indexer", "micromamba_download_url", render_mode, &err); + } + + err = 0; if (!globals.wheel_builder_manylinux_image) { globals.wheel_builder_manylinux_image = ini_getval_str(cfg, "default", "wheel_builder_manylinux_image", render_mode, &err); } @@ -35,6 +35,15 @@ wheel_builder = manylinux ; When wheel_builder is set to "manylinux", use the following image wheel_builder_manylinux_image = quay.io/pypa/manylinux2014 + +;[indexer] +; (string) Micromamba download URL +; DEFAULT: GitHub or Micromamba's primary website URLs are used +; NOTE: +; - The platform and architecture combination must be handled by the user +;micromamba_download_url = https://github.com/mamba-org/micromamba-releases/releases/latest/download/micromamba-PLAT-ARCH + + [jfrog_cli_download] url = https://releases.jfrog.io/artifactory product = jfrog-cli diff --git a/tests/test_conda.c b/tests/test_conda.c index bbbef3c..4d0b4d8 100644 --- a/tests/test_conda.c +++ b/tests/test_conda.c @@ -26,6 +26,7 @@ void test_micromamba() { for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { struct testcase *item = &tc[i]; + STASIS_ASSERT(micromamba_install(&item->mminfo) == 0, "micromamba installation failed"); int result = micromamba(&item->mminfo, (char *) item->cmd); if (result > 0) { result = result >> 8; |
