Skip to content
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

[tycho-4.0.x] Add support for building sub-bundle with tycho-bnd-extension #4667

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,46 +71,66 @@ public class BndMavenLifecycleParticipant extends AbstractMavenLifecycleParticip
@Override
public void afterProjectsRead(MavenSession session) throws MavenExecutionException {
Map<MavenProject, Project> bndProjects = getProjects(session);
Map<String, MavenProject> manifestFirstProjects = getManifestFirstProjects(session, bndProjects.keySet());
Map<String, MavenProject> bndWorkspaceProjects = bndProjects.entrySet().stream()
.collect(Collectors.toMap(e -> e.getValue().getName(), Entry<MavenProject, Project>::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<String, BndMavenProject> manifestFirstProjects = getManifestFirstProjects(session, bndProjects.keySet());
Map<String, BndMavenProject> bndWorkspaceProjects = new HashMap<>();
for (Entry<MavenProject, Project> entry : bndProjects.entrySet()) {
MavenProject mavenProject = entry.getKey();
Project project = entry.getValue();
logger.debug("==" + mavenProject.getId() + "==");
List<SubProject> 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<MavenProject, Set<String>> dependencyMap = new HashMap<>();
for (Entry<MavenProject, Project> entry : bndProjects.entrySet()) {
MavenProject mavenProject = entry.getKey();
Set<String> added = getProjectSet(mavenProject, dependencyMap);
Project bndProject = entry.getValue();
try {

for (Entry<String, String> mapping : BND_TO_MAVEN_MAPPING) {
Set<String> 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();
dependency.setGroupId(requiredMavenProject.getGroupId());
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<String> 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<String, MavenProject> getManifestFirstProjects(MavenSession session, Set<MavenProject> existing) {
Map<String, MavenProject> result = new HashMap<String, MavenProject>();
private Map<String, BndMavenProject> getManifestFirstProjects(MavenSession session, Set<MavenProject> existing) {
Map<String, BndMavenProject> result = new HashMap<>();
for (MavenProject mavenProject : session.getProjects()) {
if (existing.contains(mavenProject)) {
continue;
@@ -155,7 +177,7 @@ private Map<String, MavenProject> 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<String> getProjectSet(MavenProject mavenProject, Map<MavenProject, Set<String>> dependencyMap) {
Set<String> 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<String> 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(), "");
}

}
Original file line number Diff line number Diff line change
@@ -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) {

}
Original file line number Diff line number Diff line change
@@ -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<String> warnings = report.getWarnings();
for (String warning : warnings) {
getLog().warn(warning);
Original file line number Diff line number Diff line change
@@ -13,24 +13,51 @@
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<SubProject> 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));
file.getParentFile().mkdirs();
jar.write(file);
helper.attachArtifact(mavenProject, "jar", name, file);
}
}

}

}
Original file line number Diff line number Diff line change
@@ -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<Dependency> 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);
}
}
}

}
Original file line number Diff line number Diff line change
@@ -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");