-
-
Notifications
You must be signed in to change notification settings - Fork 4
Deterministic Bundle Id generation #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8cb06c6
1f3dcdc
dee524e
635500b
40d479a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,15 @@ | |
| import io.sentry.cli.SentryCliRunner; | ||
| import io.sentry.telemetry.SentryTelemetryService; | ||
| import java.io.*; | ||
| import java.nio.charset.Charset; | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.StandardCopyOption; | ||
| import java.security.MessageDigest; | ||
| import java.security.NoSuchAlgorithmException; | ||
| import java.util.*; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
| import org.apache.maven.execution.MavenSession; | ||
| import org.apache.maven.model.Resource; | ||
|
|
@@ -73,6 +77,9 @@ public class UploadSourceBundleMojo extends AbstractMojo { | |
| @Parameter(defaultValue = DEFAULT_IGNORE_SOURCE_BUNDLE_UPLOAD_FAILURE_STRING) | ||
| private boolean ignoreSourceBundleUploadFailure; | ||
|
|
||
| @Parameter(defaultValue = DEFAULT_REPRODUCIBLE_BUNDLE_ID_STRING) | ||
| private boolean reproducibleBundleId; | ||
|
|
||
| @SuppressWarnings("NullAway") | ||
| @Component | ||
| private @NotNull BuildPluginManager pluginManager; | ||
|
|
@@ -85,20 +92,27 @@ public void execute() throws MojoExecutionException { | |
| return; | ||
| } | ||
|
|
||
| final @NotNull String bundleId = UUID.randomUUID().toString(); | ||
| final @NotNull File collectedSourcesTargetDir = new File(sentryBuildDir(), "collected-sources"); | ||
| final @NotNull File sourceBundleTargetDir = new File(sentryBuildDir(), "source-bundle"); | ||
| final @NotNull SentryCliRunner cliRunner = | ||
| new SentryCliRunner( | ||
| debugSentryCli, sentryCliExecutablePath, mavenProject, mavenSession, pluginManager); | ||
|
|
||
| collectSources(collectedSourcesTargetDir); | ||
|
|
||
| final @NotNull String bundleId; | ||
| if (reproducibleBundleId) { | ||
| bundleId = generateDeterministicBundleId(collectedSourcesTargetDir); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There's quite a few |
||
| } else { | ||
| bundleId = UUID.randomUUID().toString(); | ||
| } | ||
|
|
||
| createDebugMetaPropertiesFile(bundleId); | ||
| collectSources(bundleId, collectedSourcesTargetDir); | ||
| bundleSources(cliRunner, bundleId, collectedSourcesTargetDir, sourceBundleTargetDir); | ||
| uploadSourceBundle(cliRunner, sourceBundleTargetDir); | ||
| } | ||
|
|
||
| private void collectSources(@NotNull String bundleId, @NotNull File outputDir) { | ||
| private void collectSources(@NotNull File outputDir) { | ||
| final @Nullable ISpan span = SentryTelemetryService.getInstance().startTask("collectSources"); | ||
| logger.debug("Collecting files from source directories"); | ||
|
|
||
|
|
@@ -168,6 +182,81 @@ private void collectSources(@NotNull String bundleId, @NotNull File outputDir) { | |
| return new File(outputDirectory, "sentry"); | ||
| } | ||
|
|
||
| /** | ||
| * Generates a deterministic bundle ID based on the MD5 hash of all collected source files. This | ||
| * ensures reproducible builds produce the same bundle ID when the source files are identical. | ||
| * | ||
| * @param collectedSourcesDir the directory containing the collected source files | ||
| * @return a UUID v4 string derived from the hash of the source files | ||
| */ | ||
| private @NotNull String generateDeterministicBundleId(final @NotNull File collectedSourcesDir) { | ||
| final @Nullable ISpan span = | ||
| SentryTelemetryService.getInstance().startTask("generateDeterministicBundleId"); | ||
| try { | ||
| final @NotNull MessageDigest digest = MessageDigest.getInstance("MD5"); | ||
|
|
||
| if (collectedSourcesDir.exists() && collectedSourcesDir.isDirectory()) { | ||
| try (final @NotNull Stream<Path> stream = Files.walk(collectedSourcesDir.toPath())) { | ||
| final @NotNull List<Path> sortedFiles = | ||
| stream | ||
| .filter(Files::isRegularFile) | ||
| .sorted( | ||
| Comparator.comparing( | ||
| p -> | ||
| collectedSourcesDir | ||
| .toPath() | ||
| .relativize(p) | ||
| .toString() | ||
| .replace('\\', '/'))) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| for (final @NotNull Path file : sortedFiles) { | ||
| final @NotNull String relativePath = | ||
| collectedSourcesDir.toPath().relativize(file).toString().replace('\\', '/'); | ||
| digest.update(relativePath.getBytes(StandardCharsets.UTF_8)); | ||
|
|
||
| // Include the file content in the hash | ||
| final byte[] fileBytes = Files.readAllBytes(file); | ||
| digest.update(fileBytes); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing delimiter between path and content in hashMedium Severity The hash computation feeds |
||
| } | ||
| } | ||
| } | ||
|
|
||
| final byte[] hashBytes = digest.digest(); | ||
| return bytesToUuid(hashBytes); | ||
| } catch (NoSuchAlgorithmException e) { | ||
| logger.warn("MD5 algorithm not available, falling back to random UUID", e); | ||
| return UUID.randomUUID().toString(); | ||
| } catch (IOException e) { | ||
| logger.warn( | ||
| "Failed to read source files for bundle ID generation, falling back to random UUID", e); | ||
| SentryTelemetryService.getInstance().captureError(e, "generateDeterministicBundleId"); | ||
| return UUID.randomUUID().toString(); | ||
| } catch (Throwable t) { | ||
| logger.warn("Failed to generate deterministic bundle ID, falling back to random UUID", t); | ||
| SentryTelemetryService.getInstance().captureError(t, "generateDeterministicBundleId"); | ||
| return UUID.randomUUID().toString(); | ||
| } finally { | ||
| SentryTelemetryService.getInstance().endTask(span); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Converts 16 bytes into a UUID v4 string format (RFC 4122). | ||
| * | ||
| * @param hashBytes the hash bytes (exactly 16 bytes expected from MD5) | ||
| * @return a UUID string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx | ||
| */ | ||
| private @NotNull String bytesToUuid(final byte[] hashBytes) { | ||
| // Set version 4 (bits 12-15 of time_hi_and_version to 0100) | ||
| hashBytes[6] = (byte) ((hashBytes[6] & 0x0F) | 0x40); | ||
| // Set variant to RFC 4122 (bits 6-7 of clock_seq_hi_and_reserved to 10) | ||
| hashBytes[8] = (byte) ((hashBytes[8] & 0x3F) | 0x80); | ||
|
|
||
| final @NotNull ByteBuffer buffer = ByteBuffer.wrap(hashBytes); | ||
| return new UUID(buffer.getLong(), buffer.getLong()).toString(); | ||
| } | ||
|
|
||
| private void bundleSources( | ||
| final @NotNull SentryCliRunner cliRunner, | ||
| final @NotNull String bundleId, | ||
|
|
@@ -280,11 +369,17 @@ private void createDebugMetaPropertiesFile(final @NotNull String bundleId) | |
| } | ||
|
|
||
| final @NotNull File debugMetaFile = new File(sentryBuildDir, "sentry-debug-meta.properties"); | ||
| final @NotNull Properties properties = createDebugMetaProperties(bundleId); | ||
|
|
||
| try (final @NotNull BufferedWriter fileWriter = | ||
| Files.newBufferedWriter(debugMetaFile.toPath(), Charset.defaultCharset())) { | ||
| properties.store(fileWriter, "Generated by sentry-maven-plugin"); | ||
| Files.newBufferedWriter(debugMetaFile.toPath(), StandardCharsets.UTF_8)) { | ||
| // Write properties without timestamp comment for reproducible builds | ||
| // Properties are written in sorted order for consistency | ||
| fileWriter.write("# Generated by sentry-maven-plugin"); | ||
| fileWriter.newLine(); | ||
| fileWriter.write("io.sentry.build-tool=maven"); | ||
| fileWriter.newLine(); | ||
| fileWriter.write("io.sentry.bundle-ids=" + bundleId); | ||
| fileWriter.newLine(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Platform-dependent newLine breaks cross-OS reproducibilityMedium Severity
|
||
|
|
||
| final @NotNull Resource resource = new Resource(); | ||
| resource.setDirectory(sentryBuildDir.getPath()); | ||
|
|
@@ -300,13 +395,4 @@ private void createDebugMetaPropertiesFile(final @NotNull String bundleId) | |
| SentryTelemetryService.getInstance().endTask(span); | ||
| } | ||
| } | ||
|
|
||
| private @NotNull Properties createDebugMetaProperties(final @NotNull String bundleId) { | ||
| final @NotNull Properties properties = new Properties(); | ||
|
|
||
| properties.setProperty("io.sentry.bundle-ids", bundleId); | ||
| properties.setProperty("io.sentry.build-tool", "maven"); | ||
|
|
||
| return properties; | ||
| } | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mWe should mention the name of the option to enable it