diff --git a/tycho-core/src/main/java/org/eclipse/m2e/pde/target/shared/MavenBundleWrapper.java b/tycho-core/src/main/java/org/eclipse/m2e/pde/target/shared/MavenBundleWrapper.java index ff89a5086e..fb5547f4a2 100644 --- a/tycho-core/src/main/java/org/eclipse/m2e/pde/target/shared/MavenBundleWrapper.java +++ b/tycho-core/src/main/java/org/eclipse/m2e/pde/target/shared/MavenBundleWrapper.java @@ -397,4 +397,50 @@ public static void transferJarEntries(File source, Manifest manifest, File targe public static boolean isValidSourceManifest(Manifest manifest) { return manifest != null && manifest.getMainAttributes().getValue(ECLIPSE_SOURCE_BUNDLE_HEADER) != null; } + + /** + * Creates or returns a cached Eclipse source bundle from a source JAR file. The resulting + * bundle will have proper Eclipse-SourceBundle manifest headers. The cached file is stored + * alongside the original source file with "-eclipse-source" suffix. + * + * @param sourceFile + * the original source JAR file (e.g., xyz-source.jar) + * @param manifest + * the manifest to use (will be modified with source bundle metadata) + * @param symbolicName + * the bundle symbolic name of the host bundle + * @param bundleVersion + * the version of the host bundle + * @return the cached eclipse source bundle file + * @throws IOException + * if reading/writing files fails + */ + public static File getEclipseSourceBundle(File sourceFile, Manifest manifest, String symbolicName, + String bundleVersion) throws IOException { + // Create the cached file name: xyz-source.jar -> xyz-eclipse-source.jar + String sourceName = sourceFile.getName(); + String eclipseSourceName; + if (sourceName.endsWith(".jar")) { + eclipseSourceName = sourceName.substring(0, sourceName.length() - 4) + "-eclipse-source.jar"; + } else { + eclipseSourceName = sourceName + "-eclipse-source"; + } + File eclipseSourceFile = new File(sourceFile.getParentFile(), eclipseSourceName); + Path eclipseSourcePath = eclipseSourceFile.toPath(); + Path sourceFilePath = sourceFile.toPath(); + + // Check if cached file exists and is up-to-date + if (!isOutdated(eclipseSourcePath, sourceFilePath)) { + return eclipseSourceFile; + } + + // Generate new eclipse source bundle + addSourceBundleMetadata(manifest, symbolicName, bundleVersion); + transferJarEntries(sourceFile, manifest, eclipseSourceFile); + + // Set the last modified time to match source file for cache validation + Files.setLastModifiedTime(eclipseSourcePath, Files.getLastModifiedTime(sourceFilePath)); + + return eclipseSourceFile; + } } diff --git a/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/MavenTargetDefinitionContent.java b/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/MavenTargetDefinitionContent.java index b43966da2c..c7a6303f07 100644 --- a/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/MavenTargetDefinitionContent.java +++ b/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/MavenTargetDefinitionContent.java @@ -432,11 +432,9 @@ private static void stripRuntimeOSGiHeaders(Manifest manifest, String symbolicNa private IInstallableUnit generateSourceBundle(String symbolicName, String bundleVersion, Manifest manifest, File sourceFile, IArtifactFacade sourceArtifact, MavenLogger logger) throws IOException, BundleException { stripRuntimeOSGiHeaders(manifest, symbolicName, bundleVersion, logger); - File tempFile = File.createTempFile("tycho_wrapped_source", ".jar"); - tempFile.deleteOnExit(); - MavenBundleWrapper.addSourceBundleMetadata(manifest, symbolicName, bundleVersion); - MavenBundleWrapper.transferJarEntries(sourceFile, manifest, tempFile); - return publish(BundlesAction.createBundleDescription(tempFile), tempFile, sourceArtifact); + File eclipseSourceFile = MavenBundleWrapper.getEclipseSourceBundle(sourceFile, manifest, symbolicName, + bundleVersion); + return publish(BundlesAction.createBundleDescription(eclipseSourceFile), eclipseSourceFile, sourceArtifact); } diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/shared/MavenBundleWrapperTest.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/shared/MavenBundleWrapperTest.java new file mode 100644 index 0000000000..24c030a443 --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/shared/MavenBundleWrapperTest.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * Copyright (c) 2025 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.shared; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Tests for {@link MavenBundleWrapper} functionality. + */ +public class MavenBundleWrapperTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testGetEclipseSourceBundle_createsNewBundle() throws Exception { + // Create a source JAR file + File sourceFile = temporaryFolder.newFile("test-source.jar"); + createSourceJar(sourceFile); + + Manifest manifest = new Manifest(); + File result = MavenBundleWrapper.getEclipseSourceBundle(sourceFile, manifest, "com.example.bundle", "1.0.0"); + + assertNotNull(result); + assertTrue("Eclipse source bundle should exist", result.exists()); + assertEquals("test-source-eclipse-source.jar", result.getName()); + assertEquals(sourceFile.getParentFile(), result.getParentFile()); + + // Verify manifest headers + try (JarFile jar = new JarFile(result)) { + Manifest resultManifest = jar.getManifest(); + assertNotNull(resultManifest); + Attributes attrs = resultManifest.getMainAttributes(); + assertEquals("com.example.bundle.source", attrs.getValue("Bundle-SymbolicName")); + assertEquals("1.0.0", attrs.getValue("Bundle-Version")); + assertTrue(attrs.getValue("Eclipse-SourceBundle").contains("com.example.bundle")); + } + } + + @Test + public void testGetEclipseSourceBundle_returnsCache() throws Exception { + // Create a source JAR file + File sourceFile = temporaryFolder.newFile("cached-source.jar"); + createSourceJar(sourceFile); + + Manifest manifest1 = new Manifest(); + File result1 = MavenBundleWrapper.getEclipseSourceBundle(sourceFile, manifest1, "com.example.bundle", "1.0.0"); + + // Remember modification time + long firstModTime = result1.lastModified(); + + // Wait a bit to ensure timestamp would be different if regenerated + Thread.sleep(100); + + // Call again - should return cached version + Manifest manifest2 = new Manifest(); + File result2 = MavenBundleWrapper.getEclipseSourceBundle(sourceFile, manifest2, "com.example.bundle", "1.0.0"); + + assertEquals(result1.getAbsolutePath(), result2.getAbsolutePath()); + assertEquals("Cache should be reused, modification time should be unchanged", firstModTime, + result2.lastModified()); + } + + @Test + public void testGetEclipseSourceBundle_regeneratesWhenSourceChanged() throws Exception { + // Create a source JAR file + File sourceFile = temporaryFolder.newFile("changing-source.jar"); + createSourceJar(sourceFile); + + Manifest manifest1 = new Manifest(); + File result1 = MavenBundleWrapper.getEclipseSourceBundle(sourceFile, manifest1, "com.example.bundle", "1.0.0"); + long firstSize = result1.length(); + + // Modify the source file + Thread.sleep(100); // Ensure different timestamp + createSourceJar(sourceFile, "additional content"); + + // Update source file timestamp to be newer + sourceFile.setLastModified(System.currentTimeMillis()); + + Manifest manifest2 = new Manifest(); + File result2 = MavenBundleWrapper.getEclipseSourceBundle(sourceFile, manifest2, "com.example.bundle", "1.0.0"); + + assertEquals(result1.getAbsolutePath(), result2.getAbsolutePath()); + // The file should be regenerated (different size due to different content) + assertTrue("Regenerated bundle should have different size", result2.length() != firstSize); + } + + @Test + public void testIsOutdated_returnsTrueForMissingCache() throws Exception { + File sourceFile = temporaryFolder.newFile("source.jar"); + File cacheFile = new File(temporaryFolder.getRoot(), "cache.jar"); + + assertTrue("Missing cache file should be outdated", + MavenBundleWrapper.isOutdated(cacheFile.toPath(), sourceFile.toPath())); + } + + @Test + public void testIsOutdated_returnsFalseForUpToDateCache() throws Exception { + File sourceFile = temporaryFolder.newFile("source.jar"); + File cacheFile = temporaryFolder.newFile("cache.jar"); + + // Set cache to be same timestamp as source + cacheFile.setLastModified(sourceFile.lastModified()); + + assertFalse("Cache with same timestamp should not be outdated", + MavenBundleWrapper.isOutdated(cacheFile.toPath(), sourceFile.toPath())); + } + + @Test + public void testIsOutdated_returnsTrueWhenSourceNewer() throws Exception { + File sourceFile = temporaryFolder.newFile("source.jar"); + File cacheFile = temporaryFolder.newFile("cache.jar"); + + // Set source to be newer than cache + Thread.sleep(100); + sourceFile.setLastModified(System.currentTimeMillis()); + cacheFile.setLastModified(sourceFile.lastModified() - 1000); + + assertTrue("Cache should be outdated when source is newer", + MavenBundleWrapper.isOutdated(cacheFile.toPath(), sourceFile.toPath())); + } + + private void createSourceJar(File file) throws IOException { + createSourceJar(file, null); + } + + private void createSourceJar(File file, String additionalContent) throws IOException { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(file), manifest)) { + // Add a dummy source file + jos.putNextEntry(new ZipEntry("com/example/Test.java")); + String content = "package com.example;\npublic class Test {}"; + if (additionalContent != null) { + content += "\n// " + additionalContent; + } + jos.write(content.getBytes()); + jos.closeEntry(); + } + } +}