diff --git a/.gitignore b/.gitignore index 79eeb49e6b..2b8dedab5a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ bin/ *.iml .idea/ .tycho-consumer-pom.xml +.polyglot.* ## Eclipse metadata files org.eclipse.core.resources.prefs org.eclipse.m2e.core.prefs @@ -21,3 +22,4 @@ pom-model-classic.xml project-dependencies.xml project-units.xml requirements.txt +.DS_Store \ No newline at end of file diff --git a/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/maven/BndMavenLifecycleParticipant.java b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/maven/BndMavenLifecycleParticipant.java index 3e197b0b95..e7bfd7084d 100644 --- a/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/maven/BndMavenLifecycleParticipant.java +++ b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/maven/BndMavenLifecycleParticipant.java @@ -16,13 +16,14 @@ import java.io.FileInputStream; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.jar.JarFile; import java.util.jar.Manifest; -import java.util.stream.Collectors; import org.apache.maven.AbstractMavenLifecycleParticipant; import org.apache.maven.MavenExecutionException; @@ -36,6 +37,7 @@ import org.eclipse.tycho.core.bnd.BndPluginManager; import aQute.bnd.build.Project; +import aQute.bnd.build.SubProject; import aQute.bnd.build.Workspace; import aQute.bnd.osgi.Constants; @@ -69,31 +71,42 @@ public class BndMavenLifecycleParticipant extends AbstractMavenLifecycleParticip @Override public void afterProjectsRead(MavenSession session) throws MavenExecutionException { Map bndProjects = getProjects(session); - Map manifestFirstProjects = getManifestFirstProjects(session, bndProjects.keySet()); - Map bndWorkspaceProjects = bndProjects.entrySet().stream() - .collect(Collectors.toMap(e -> e.getValue().getName(), Entry::getKey, (a, b) -> { - logger.warn( - "Your reactor build contains duplicate BND projects from different workspace, build order might be insufficient!"); - logger.warn("\tProject 1 (selected): " + a.getBasedir()); - logger.warn("\tProject 2 (ignored): " + b.getBasedir()); - return a; - })); + Map manifestFirstProjects = getManifestFirstProjects(session, bndProjects.keySet()); + Map bndWorkspaceProjects = new HashMap<>(); for (Entry entry : bndProjects.entrySet()) { MavenProject mavenProject = entry.getKey(); + Project project = entry.getValue(); + logger.debug("==" + mavenProject.getId() + "=="); + List subProjects = project.getSubProjects(); + logger.debug("Main: " + project.getName()); + if (subProjects.isEmpty()) { + bndWorkspaceProjects.put(project.getName(), new BndMavenProject(mavenProject, project, null)); + } else { + for (SubProject subProject : subProjects) { + logger.debug("Sub: " + subProject.getName()); + bndWorkspaceProjects.put(project.getName() + "." + subProject.getName(), + new BndMavenProject(mavenProject, project, subProject.getName())); + } + } + } + Map> dependencyMap = new HashMap<>(); + for (Entry entry : bndProjects.entrySet()) { + MavenProject mavenProject = entry.getKey(); + Set added = getProjectSet(mavenProject, dependencyMap); Project bndProject = entry.getValue(); try { - for (Entry mapping : BND_TO_MAVEN_MAPPING) { Set requirements = bndProject.getMergedParameters(mapping.getKey()).keySet(); String mavenScope = mapping.getValue(); for (String required : requirements) { - MavenProject requiredMavenProject = bndWorkspaceProjects.get(required); - if (requiredMavenProject == null) { - requiredMavenProject = manifestFirstProjects.get(required); + BndMavenProject bndMavenProject = bndWorkspaceProjects.get(required); + if (bndMavenProject == null) { + bndMavenProject = manifestFirstProjects.get(required); } - if (requiredMavenProject == null || requiredMavenProject == mavenProject) { + if (bndMavenProject == null || bndMavenProject.mavenProject() == mavenProject) { continue; } + MavenProject requiredMavenProject = bndMavenProject.mavenProject(); logger.debug(mavenProject.getId() + " depends on reactor project " + requiredMavenProject.getId() + " ..."); Dependency dependency = new Dependency(); @@ -101,14 +114,23 @@ public void afterProjectsRead(MavenSession session) throws MavenExecutionExcepti dependency.setArtifactId(requiredMavenProject.getArtifactId()); dependency.setVersion(requiredMavenProject.getVersion()); dependency.setScope(mavenScope); - mavenProject.getDependencies().add(dependency); + if (bndMavenProject.classifier() != null) { + Dependency clone = dependency.clone(); + clone.setClassifier(bndMavenProject.classifier()); + clone.setType("jar"); + addDependency(mavenProject, clone, added); + } + dependency.setType(requiredMavenProject.getPackaging()); + addDependency(mavenProject, dependency, added); } } } catch (Exception e) { logError("Can't get dependents of project " + mavenProject.getId(), e); } } - for (MavenProject mavenProject : manifestFirstProjects.values()) { + for (BndMavenProject bndMavenProject : manifestFirstProjects.values()) { + MavenProject mavenProject = bndMavenProject.mavenProject(); + Set added = getProjectSet(mavenProject, dependencyMap); try { File file = new File(mavenProject.getBasedir(), "build.properties"); if (file.isFile()) { @@ -121,17 +143,17 @@ public void afterProjectsRead(MavenSession session) throws MavenExecutionExcepti continue; } for (String bundle : property.split(",")) { - MavenProject requiredMavenProject = bndWorkspaceProjects.get(bundle.trim()); - if (requiredMavenProject == null || requiredMavenProject == mavenProject) { + BndMavenProject requiredMavenProject = bndWorkspaceProjects.get(bundle.trim()); + if (requiredMavenProject == null || requiredMavenProject.mavenProject() == mavenProject) { continue; } logger.debug(mavenProject.getId() + " depends on reactor project " - + requiredMavenProject.getId() + " ..."); + + requiredMavenProject.mavenProject().getId() + " ..."); Dependency dependency = new Dependency(); - dependency.setGroupId(requiredMavenProject.getGroupId()); - dependency.setArtifactId(requiredMavenProject.getArtifactId()); - dependency.setVersion(requiredMavenProject.getVersion()); - mavenProject.getDependencies().add(dependency); + dependency.setGroupId(requiredMavenProject.mavenProject().getGroupId()); + dependency.setArtifactId(requiredMavenProject.mavenProject().getArtifactId()); + dependency.setVersion(requiredMavenProject.mavenProject().getVersion()); + addDependency(mavenProject, dependency, added); } } } catch (Exception e) { @@ -141,8 +163,8 @@ public void afterProjectsRead(MavenSession session) throws MavenExecutionExcepti } } - private Map getManifestFirstProjects(MavenSession session, Set existing) { - Map result = new HashMap(); + private Map getManifestFirstProjects(MavenSession session, Set existing) { + Map result = new HashMap<>(); for (MavenProject mavenProject : session.getProjects()) { if (existing.contains(mavenProject)) { continue; @@ -155,7 +177,7 @@ private Map getManifestFirstProjects(MavenSession session, String value = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME); if (value != null) { String bsn = value.split(";", 2)[0]; - result.put(bsn, mavenProject); + result.put(bsn, new BndMavenProject(mavenProject, null, null)); } } catch (Exception e) { logError("Can't read project " + mavenProject.getId(), e); @@ -208,4 +230,21 @@ private void logError(String msg, Exception e) { } } + private static Set getProjectSet(MavenProject mavenProject, Map> dependencyMap) { + Set set = dependencyMap.computeIfAbsent(mavenProject, nil -> new HashSet<>()); + mavenProject.getDependencies().stream().map(d -> getKey(d)).forEach(set::add); + return set; + } + + private static void addDependency(MavenProject mavenProject, Dependency dependency, Set added) { + if (added.add(getKey(dependency))) { + mavenProject.getDependencies().add(dependency); + } + } + + private static String getKey(Dependency dependency) { + return dependency.getManagementKey() + ":" + dependency.getVersion() + ":" + + Objects.requireNonNullElse(dependency.getClassifier(), ""); + } + } diff --git a/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/maven/BndMavenProject.java b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/maven/BndMavenProject.java new file mode 100644 index 0000000000..bf6020a0fe --- /dev/null +++ b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/maven/BndMavenProject.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 accompanies this distribution, and 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.tycho.bnd.maven; + +import org.apache.maven.project.MavenProject; + +import aQute.bnd.build.Project; + +record BndMavenProject(MavenProject mavenProject, Project project, String classifier) { + +} diff --git a/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/AbstractBndProjectMojo.java b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/AbstractBndProjectMojo.java index 7da608e977..ee7aca8794 100644 --- a/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/AbstractBndProjectMojo.java +++ b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/AbstractBndProjectMojo.java @@ -59,7 +59,7 @@ public final void execute() throws MojoExecutionException, MojoFailureException } - private void checkResult(Report report, boolean errorOk) throws MojoFailureException { + protected void checkResult(Report report, boolean errorOk) throws MojoFailureException { List warnings = report.getWarnings(); for (String warning : warnings) { getLog().warn(warning); diff --git a/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/BndBuildMojo.java b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/BndBuildMojo.java index f7e71870c9..c742d0986c 100644 --- a/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/BndBuildMojo.java +++ b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/BndBuildMojo.java @@ -13,24 +13,50 @@ package org.eclipse.tycho.bnd.mojos; import java.io.File; +import java.util.List; import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProjectHelper; import aQute.bnd.build.Project; +import aQute.bnd.build.ProjectBuilder; +import aQute.bnd.build.SubProject; +import aQute.bnd.osgi.Jar; @Mojo(name = "build", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true) public class BndBuildMojo extends AbstractBndProjectMojo { + @Component + MavenProjectHelper helper; + @Override protected void execute(Project project) throws Exception { - File[] files = project.build(); - if (files != null && files.length > 0) { - Artifact artifact = mavenProject.getArtifact(); - artifact.setFile(files[0]); + List subProjects = project.getSubProjects(); + if (subProjects.isEmpty()) { + getLog().info(String.format("Building bundle '%s'", project.getName())); + File[] files = project.build(); + if (files != null && files.length > 0) { + Artifact artifact = mavenProject.getArtifact(); + artifact.setFile(files[0]); + } + } else { + for (SubProject subProject : subProjects) { + ProjectBuilder builder = subProject.getProjectBuilder(); + getLog().info(String.format("Building sub bundle '%s'", subProject.getName())); + Jar jar = builder.build(); + checkResult(builder, project.getWorkspace().isFailOk()); + String jarName = jar.getName(); + String name = subProject.getName(); + File file = new File(mavenProject.getBuild().getDirectory(), String.format("%s.jar", jarName)); + jar.write(file); + helper.attachArtifact(mavenProject, "jar", name, file); + } } + } } diff --git a/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/BndInitMojo.java b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/BndInitMojo.java new file mode 100644 index 0000000000..37851c61b1 --- /dev/null +++ b/tycho-bnd-plugin/src/main/java/org/eclipse/tycho/bnd/mojos/BndInitMojo.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * 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 accompanies this distribution, and 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.tycho.bnd.mojos; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.ModelReader; +import org.apache.maven.model.io.ModelWriter; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +@Mojo(name = "initialize", defaultPhase = LifecyclePhase.INITIALIZE) +public class BndInitMojo extends AbstractMojo { + + @Parameter(property = "project", readonly = true) + protected MavenProject mavenProject; + + /** + * The filename of the tycho generated POM file. + */ + @Parameter(defaultValue = ".tycho-consumer-pom.xml", property = "tycho.bnd.consumerpom.file") + protected String tychoPomFilename; + + /** + * If deleteOnExit is true the file will be marked for deletion on JVM exit + */ + @Parameter(defaultValue = "true", property = "tycho.bnd.consumerpom.delete") + protected boolean deleteOnExit = true; + + /** + * Indicate if the generated tycho POM should become the new project. + */ + @Parameter(defaultValue = "true", property = "tycho.bnd.consumerpom.update") + protected boolean updatePomFile = true; + + @Parameter(defaultValue = "false", property = "tycho.bnd.consumerpom.skip") + protected boolean skipPomGeneration; + + /** + * The directory where the tycho generated POM file will be written to. + */ + @Parameter(defaultValue = "${project.basedir}", property = "tycho.bnd.consumerpom.directory") + protected File outputDirectory; + + @Component(role = ModelWriter.class) + protected ModelWriter modelWriter; + + @Component(role = ModelReader.class) + protected ModelReader modelReader; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + fixupPolyglot(); + writeConsumerPom(); + } + + private void writeConsumerPom() throws MojoExecutionException { + if (skipPomGeneration) { + return; + } + Model projectModel; + try { + projectModel = modelReader.read(mavenProject.getFile(), Map.of()); + } catch (IOException e) { + throw new MojoExecutionException("reading the model failed!", e); + } + projectModel.setVersion(mavenProject.getVersion()); + projectModel.setGroupId(mavenProject.getGroupId()); + projectModel.setParent(null); + List dependencies = projectModel.getDependencies(); + dependencies.clear(); + dependencies.addAll(Objects.requireNonNullElse(mavenProject.getDependencies(), Collections.emptyList())); + File output = new File(outputDirectory, tychoPomFilename); + if (deleteOnExit) { + output.deleteOnExit(); + } + try { + modelWriter.write(output, Map.of(), projectModel); + } catch (IOException e) { + throw new MojoExecutionException("writing the model failed!", e); + } + if (updatePomFile) { + mavenProject.setFile(output); + } + } + + private void fixupPolyglot() { + /* + * bnd instructions might include wildcards (e.g. -sub *.bnd) these commands get + * confused if the automatic polyglot file named after the bnd file + * (.polyglot.bnd.bnd) is present and goes wild. We simply rename it here to xml + * to avoid any confusion. + */ + File basedir = mavenProject.getBasedir(); + File[] files = basedir.listFiles(new FileFilter() { + + @Override + public boolean accept(File pathname) { + return pathname.getName().startsWith(".polyglot.") && pathname.getName().endsWith(".bnd"); + } + + }); + for (File file : files) { + File moved = new File(file.getParentFile(), ".polyglot.xml"); + if (file.renameTo(moved)) { + mavenProject.setFile(moved); + } + } + } + +} diff --git a/tycho-build/src/main/java/org/eclipse/tycho/build/bnd/BndProjectMapping.java b/tycho-build/src/main/java/org/eclipse/tycho/build/bnd/BndProjectMapping.java index 399e14f8f0..25ca5462fc 100644 --- a/tycho-build/src/main/java/org/eclipse/tycho/build/bnd/BndProjectMapping.java +++ b/tycho-build/src/main/java/org/eclipse/tycho/build/bnd/BndProjectMapping.java @@ -80,6 +80,11 @@ public String getFlavour() { protected void initModel(Model model, Reader artifactReader, Path artifactFile) throws IOException { try { Project project = Workspace.getProject(artifactFile.getParent().toFile()); + if (project.getSubProjects().isEmpty()) { + model.setPackaging(getPackaging()); + } else { + model.setPackaging("pom"); + } String g = project.getProperty(TychoConstants.PROP_GROUP_ID); String a = project.getProperty(TychoConstants.PROP_ARTIFACT_ID); String v = project.getProperty(TychoConstants.PROP_VERSION); @@ -101,6 +106,10 @@ protected void initModel(Model model, Reader artifactReader, Path artifactFile) Plugin bndPlugin = getPlugin(model, TYCHO_GROUP_ID, TYCHO_BND_PLUGIN); bndPlugin.setExtensions(true); bndPlugin.setVersion(TychoVersion.getTychoVersion()); + addPluginExecution(bndPlugin, execution -> { + execution.setId("initialize"); + execution.addGoal("initialize"); + }); addPluginExecution(bndPlugin, execution -> { execution.setId("clean"); execution.addGoal("clean");