aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@gmail.com>2026-06-29 15:17:47 -0400
committerJoseph Hunkeler <jhunkeler@gmail.com>2026-06-30 09:53:50 -0400
commit456693eef1c3c2f97cf27d777e4e00464e01ff82 (patch)
tree84efefb8fa80d13daa4ee4ed0498db86d6dcc0d7
parent4a189db63597c66b15101ba0344494f49d9af3b1 (diff)
downloadstasis-456693eef1c3c2f97cf27d777e4e00464e01ff82.tar.gz
Implement --force-repeatable optionforce-repeatable
-rw-r--r--README.md1
-rw-r--r--src/cli/stasis/args.c2
-rw-r--r--src/cli/stasis/include/args.h1
-rw-r--r--src/cli/stasis/stasis_main.c3
-rw-r--r--src/lib/core/include/core.h1
-rw-r--r--src/lib/core/include/utils.h2
-rw-r--r--src/lib/core/utils.c17
-rw-r--r--src/lib/delivery/delivery_build.c24
-rw-r--r--src/lib/delivery/delivery_test.c71
9 files changed, 100 insertions, 22 deletions
diff --git a/README.md b/README.md
index caf46d0..510aafd 100644
--- a/README.md
+++ b/README.md
@@ -165,6 +165,7 @@ stasis mydelivery.ini
| --overwrite | n/a | Overwrite an existing release |
| --wheel-builder ARG | n/a | Wheel building backend (build, cibuildwheel, manylinux) |
| --wheel-builder-manylinux-image ARG | n/a | Manylinux image name |
+| --force-repeatable | n/a | Adapt package source(s) and settings to reduce changes |
| --no-docker | n/a | Do not build docker images |
| --no-artifactory | n/a | Do not upload artifacts to Artifactory |
| --no-artifactory-build-info | n/a | Do not upload build info objects to Artifactory |
diff --git a/src/cli/stasis/args.c b/src/cli/stasis/args.c
index c1bf031..d4dec0c 100644
--- a/src/cli/stasis/args.c
+++ b/src/cli/stasis/args.c
@@ -17,6 +17,7 @@ struct option long_options[] = {
{"overwrite", no_argument, 0, OPT_OVERWRITE},
{"wheel-builder", required_argument, 0, OPT_WHEEL_BUILDER},
{"wheel-builder-manylinux-image", required_argument, 0, OPT_WHEEL_BUILDER_MANYLINUX_IMAGE},
+ {"force-repeatable", no_argument, 0, OPT_FORCE_REPEATABLE},
{"no-docker", no_argument, 0, OPT_NO_DOCKER},
{"no-artifactory", no_argument, 0, OPT_NO_ARTIFACTORY},
{"no-artifactory-build-info", no_argument, 0, OPT_NO_ARTIFACTORY_BUILD_INFO},
@@ -44,6 +45,7 @@ const char *long_options_help[] = {
"Overwrite an existing release",
"Wheel building backend (build, cibuildwheel, manylinux)",
"Manylinux image name",
+ "Adapt package source(s) and settings to reduce changes",
"Do not build docker images",
"Do not upload artifacts to Artifactory",
"Do not upload build info objects to Artifactory",
diff --git a/src/cli/stasis/include/args.h b/src/cli/stasis/include/args.h
index e789261..ecb20c3 100644
--- a/src/cli/stasis/include/args.h
+++ b/src/cli/stasis/include/args.h
@@ -21,6 +21,7 @@
#define OPT_TASK_TIMEOUT 1013
#define OPT_WHEEL_BUILDER 1014
#define OPT_WHEEL_BUILDER_MANYLINUX_IMAGE 1015
+#define OPT_FORCE_REPEATABLE 1016
extern struct option long_options[];
void usage(char *progname);
diff --git a/src/cli/stasis/stasis_main.c b/src/cli/stasis/stasis_main.c
index c5c1f00..5a9f694 100644
--- a/src/cli/stasis/stasis_main.c
+++ b/src/cli/stasis/stasis_main.c
@@ -638,6 +638,9 @@ int main(const int argc, char *argv[]) {
case OPT_WHEEL_BUILDER_MANYLINUX_IMAGE:
globals.wheel_builder_manylinux_image = strdup(optarg);
break;
+ case OPT_FORCE_REPEATABLE:
+ globals.force_repeatable = true;
+ break;
case '?':
default:
exit(1);
diff --git a/src/lib/core/include/core.h b/src/lib/core/include/core.h
index ed523be..e3e54c9 100644
--- a/src/lib/core/include/core.h
+++ b/src/lib/core/include/core.h
@@ -57,6 +57,7 @@ struct STASIS_GLOBAL {
int task_timeout; ///!< Time in seconds before task is terminated
char *wheel_builder; ///!< Backend to build wheels (build, cibuildwheel, manylinux)
char *wheel_builder_manylinux_image; ///!< Image to use for a Manylinux build
+ bool force_repeatable; ///!< Reduces surface area of random changes between builds
struct {
char *tox_posargs;
char *conda_reactivate;
diff --git a/src/lib/core/include/utils.h b/src/lib/core/include/utils.h
index e75995a..c7865eb 100644
--- a/src/lib/core/include/utils.h
+++ b/src/lib/core/include/utils.h
@@ -473,7 +473,7 @@ int in_ascii_range(char c, char lower, char upper);
#define GIT_HASH_LEN 40
int is_git_sha(char const *hash);
-int check_python_package_dependencies(const char *srcdir);
+int check_python_package_dependencies(const char *srcdir, struct StrList **out_files, struct StrList **out_matches);
void seconds_to_human_readable(int v, char *result, size_t maxlen);
diff --git a/src/lib/core/utils.c b/src/lib/core/utils.c
index 462604d..d0ccb08 100644
--- a/src/lib/core/utils.c
+++ b/src/lib/core/utils.c
@@ -1078,7 +1078,7 @@ static int read_vcs_records(const size_t line, char **data) {
// no match, continue
return 1;
}
-int check_python_package_dependencies(const char *srcdir) {
+int check_python_package_dependencies(const char *srcdir, struct StrList **out_files, struct StrList **out_matches) {
const char *configs[] = {
"pyproject.toml",
"setup.cfg",
@@ -1103,12 +1103,25 @@ int check_python_package_dependencies(const char *srcdir) {
}
const size_t count = strlist_count(data);
if (count) {
+ if (out_files) {
+ strlist_append(out_files, (char *) configs[i]);
+ }
printf("\nERROR: VCS requirement(s) detected in %s:\n", configfile);
for (size_t j = 0; j < count; j++) {
char *record = strlist_item(data, j);
lstrip(record);
strip(record);
- printf("[%zu] %s\n", j, record);
+ char *match = substring_between(record, "\"\"");
+ if (!match) {
+ SYSERROR("unable to allocate bytes for matched sub-string");
+ guard_strlist_free(&data);
+ return -1;
+ }
+ if (out_matches) {
+ strlist_append(out_matches, match);
+ }
+ printf("[%zu] %s\n", j, match);
+ guard_free(match);
}
guard_strlist_free(&data);
return 1;
diff --git a/src/lib/delivery/delivery_build.c b/src/lib/delivery/delivery_build.c
index 7ea5b29..74ea77d 100644
--- a/src/lib/delivery/delivery_build.c
+++ b/src/lib/delivery/delivery_build.c
@@ -448,16 +448,22 @@ struct StrList *delivery_build_wheels(struct Delivery *ctx) {
memset(dname, 0, sizeof(dname));
memset(outdir, 0, sizeof(outdir));
- const int dep_status = check_python_package_dependencies(".");
+ const int dep_status = check_python_package_dependencies(".", NULL, NULL);
if (dep_status) {
- SYSERROR("Please replace all occurrences above with standard package specs:\n"
- "\n"
- " package==x.y.z\n"
- " package>=x.y.z\n"
- " package<=x.y.z\n"
- " ...\n"
- "\n");
- COE_CHECK_ABORT(true, "Unreproducible delivery");
+ const char *warning_message = "Please replace all occurrences above with standard package specs:\n"
+ "\n"
+ " package==x.y.z\n"
+ " package>=x.y.z\n"
+ " package<=x.y.z\n"
+ " ...\n"
+ "\n";
+ if (globals.force_repeatable) {
+ SYSWARN("--force-repeatable is enabled");
+ SYSWARN(warning_message);
+ } else {
+ SYSERROR(warning_message);
+ COE_CHECK_ABORT(true, "Unreproducible delivery");
+ }
}
safe_strncpy(dname, ctx->tests->test[i]->name, sizeof(dname));
diff --git a/src/lib/delivery/delivery_test.c b/src/lib/delivery/delivery_test.c
index 4ea3b3d..f8da208 100644
--- a/src/lib/delivery/delivery_test.c
+++ b/src/lib/delivery/delivery_test.c
@@ -178,17 +178,69 @@ void delivery_tests_run(struct Delivery *ctx) {
if (pushd(destdir)) {
COE_CHECK_ABORT(1, "Unable to enter repository directory");
} else {
- const int dep_status = check_python_package_dependencies(".");
+ struct StrList *setup_files = strlist_init();
+ if (!setup_files) {
+ SYSERROR("unable to to allocate bytes for setup file list");
+ exit(1);
+ }
+
+ struct StrList *matches = strlist_init();
+ if (!matches) {
+ SYSERROR("unable to allocate bytes for matches list");
+ guard_strlist_free(&setup_files);
+ exit(1);
+ }
+
+ const int dep_status = check_python_package_dependencies(".", &setup_files, &matches);
if (dep_status) {
- SYSERROR("Please replace all occurrences above with standard package specs:\n"
- "\n"
- " package==x.y.z\n"
- " package>=x.y.z\n"
- " package<=x.y.z\n"
- " ...\n"
- "\n");
- COE_CHECK_ABORT(true, "Unreproducible delivery");
+ const char *warning_message = "Please replace all occurrences above with standard package specs:\n"
+ "\n"
+ " package==x.y.z\n"
+ " package>=x.y.z\n"
+ " package<=x.y.z\n"
+ " ...\n"
+ "\n";
+ if (globals.force_repeatable) {
+ SYSWARN("--force-repeatable is enabled");
+ SYSWARN(warning_message);
+ } else {
+ SYSERROR(warning_message);
+ COE_CHECK_ABORT(true, "Unreproducible delivery");
+ }
+
+ // VCS records have been found, force_repeatable or COE is enabled, so we don't care about integrity
+ // Replace the records in the file(s)
+ if (strlist_count(setup_files) && strlist_count(matches)) {
+ SYSDEBUG("Will replace %zu string(s) in %zu file(s)...", strlist_count(matches), strlist_count(setup_files));
+ for (size_t f = 0; f < strlist_count(setup_files); f++) {
+ const char *setup_file = strlist_item(setup_files, f);
+ for (size_t m = 0; m < strlist_count(matches); m++) {
+ const char *match = strlist_item(matches, m);
+
+ // copy the original match string
+ char *replacement = strdup(match);
+ if (!replacement) {
+ SYSERROR("unable to allocate bytes for replacement value");
+ exit(1);
+ }
+
+ // Truncate the replacement string at the first space character or @ symbol
+ char *stop = strpbrk(replacement, " @");
+ if (stop) {
+ *stop = '\0';
+ SYSINFO("%s: replacing '%s' with '%s'", setup_file, match, replacement);
+ if (file_replace_text(setup_file, match, replacement, 0)) {
+ SYSERROR("%s: replacement failed", setup_file);
+ exit(1);
+ }
+ }
+ guard_free(replacement);
+ }
+ }
+ }
}
+ guard_strlist_free(&setup_files);
+ guard_strlist_free(&matches);
char *cmd = calloc(strlen(test->script) + STASIS_BUFSIZ, sizeof(*cmd));
if (!cmd) {
@@ -256,7 +308,6 @@ void delivery_tests_run(struct Delivery *ctx) {
guard_free(runner_cmd);
guard_free(cmd);
popd();
-
}
}