diff --git a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysis.java b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysis.java new file mode 100644 index 0000000000..2da3262656 --- /dev/null +++ b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysis.java @@ -0,0 +1,179 @@ +/******************************************************************************* + * Copyright (c) 2023 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.apitools; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.osgi.service.resolver.ResolverError; +import org.eclipse.pde.api.tools.internal.FilterStore; +import org.eclipse.pde.api.tools.internal.builder.BaseApiAnalyzer; +import org.eclipse.pde.api.tools.internal.builder.BuildContext; +import org.eclipse.pde.api.tools.internal.model.ApiModelFactory; +import org.eclipse.pde.api.tools.internal.model.BundleComponent; +import org.eclipse.pde.api.tools.internal.model.SystemLibraryApiComponent; +import org.eclipse.pde.api.tools.internal.provisional.IApiFilterStore; +import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline; +import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent; +import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +public class ApiAnalysis implements Serializable, Callable { + + private Collection baselineBundles; + private String baselineName; + private String apiFilterFile; + private String bundleFile; + private boolean debug; + private Collection compareBundles; + private String apiPreferences; + + public ApiAnalysis(Collection baselineBundles, Collection dependencyBundles, String baselineName, + Path apiFilterFile, Path apiPreferences, Path bundleFile, boolean debug) { + this.compareBundles = Stream.concat(Stream.of(bundleFile), dependencyBundles.stream()).map(Path::toAbsolutePath) + .map(Path::toString).toList(); + this.baselineBundles = baselineBundles.stream().map(Path::toAbsolutePath).map(Path::toString).toList(); + this.baselineName = baselineName; + this.apiFilterFile = apiFilterFile == null ? null : apiFilterFile.toAbsolutePath().toString(); + this.apiPreferences = apiPreferences == null ? null : apiPreferences.toAbsolutePath().toString(); + this.bundleFile = bundleFile.toAbsolutePath().toString(); + this.debug = debug; + } + + @Override + public ApiAnalysisResult call() throws Exception { + printVersion(); + IApiBaseline baseline = createBaseline(baselineBundles, baselineName + " - baseline"); + IApiBaseline compare = createBaseline(compareBundles, baselineName + " - compare"); + BundleComponent bundle = getBundleOfInterest(compare); + ResolverError[] resolverErrors = bundle.getErrors(); + if (resolverErrors != null && resolverErrors.length > 0) { + throw new RuntimeException("The bundle has resolve errors"); + } + + IApiFilterStore filterStore = getApiFilterStore(bundle); + BaseApiAnalyzer analyzer = new BaseApiAnalyzer(); + try { + analyzer.setContinueOnResolverError(true); + analyzer.analyzeComponent(null, filterStore, getPreferences(), baseline, bundle, new BuildContext(), + new NullProgressMonitor()); + IApiProblem[] problems = analyzer.getProblems(); + for (IApiProblem problem : problems) { + // TODO add to the result... + System.out.println(problem); + } + } finally { + analyzer.dispose(); + } + return new ApiAnalysisResult(); + } + + private Properties getPreferences() throws IOException { + Properties properties = new Properties(); + if (apiPreferences != null) { + try (FileInputStream stream = new FileInputStream(apiPreferences)) { + properties.load(stream); + } + } + return properties; + } + + private BundleComponent getBundleOfInterest(IApiBaseline baseline) { + for (IApiComponent component : baseline.getApiComponents()) { + String location = component.getLocation(); + if (bundleFile.equals(location)) { + if (component instanceof BundleComponent bundle) { + return bundle; + } + } + } + throw new RuntimeException("Can't find bundle in baseline!"); + } + + private void printVersion() { + Bundle apiToolsBundle = FrameworkUtil.getBundle(ApiModelFactory.class); + if (apiToolsBundle != null) { + debug("API Tools version: " + apiToolsBundle.getVersion()); + } + } + + private IApiBaseline createBaseline(Collection bundles, String name) throws CoreException { + debug("==== " + name + " ===="); + IApiBaseline baseline = ApiModelFactory.newApiBaseline(name); + List baselineComponents = new ArrayList(); + for (String baselineBundle : bundles) { + IApiComponent component = ApiModelFactory.newApiComponent(baseline, baselineBundle); + if (component != null) { + debug(component.getSymbolicName() + " " + component.getVersion() + " -- " + + new File(component.getLocation()).getName()); + baselineComponents.add(component); + } + } + baseline.addApiComponents(baselineComponents.toArray(IApiComponent[]::new)); + for (IApiComponent component : baseline.getApiComponents()) { + if (component instanceof SystemLibraryApiComponent systemLibrary) { + debug("System Component:"); + debug("\tVersion: " + systemLibrary.getVersion()); + debug("\tLocation: " + systemLibrary.getLocation()); + for (String ee : systemLibrary.getExecutionEnvironments()) { + debug("\tExecution Environment: " + ee); + } + } + + } + return baseline; + } + + private IApiFilterStore getApiFilterStore(BundleComponent bundle) { + return new FilterStore(bundle) { + @Override + protected synchronized void initializeApiFilters() { + if (fFilterMap == null) { + fFilterMap = new HashMap<>(5); + if (apiFilterFile != null) { + Path path = Path.of(apiFilterFile); + if (Files.isRegularFile(path)) { + try (InputStream stream = Files.newInputStream(path)) { + readFilterFile(stream); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + } + }; + } + + private void debug(String string) { + if (debug) { + System.out.println(string); + } + } + +} diff --git a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisMojo.java b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisMojo.java index 48ca2904d3..e23352b235 100644 --- a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisMojo.java +++ b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisMojo.java @@ -12,14 +12,9 @@ *******************************************************************************/ package org.eclipse.tycho.apitools; -import java.io.BufferedWriter; import java.io.File; -import java.io.IOException; import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -27,7 +22,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenSession; @@ -41,6 +35,7 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; +import org.eclipse.pde.api.tools.internal.IApiCoreConstants; import org.eclipse.tycho.ArtifactDescriptor; import org.eclipse.tycho.ArtifactKey; import org.eclipse.tycho.ClasspathEntry; @@ -89,6 +84,9 @@ public class ApiAnalysisMojo extends AbstractMojo { @Parameter(defaultValue = "false", property = "tycho.apitools.verify.skip") private boolean skip; + @Parameter(defaultValue = "false", property = "tycho.apitools.debug") + private boolean debug; + @Parameter(defaultValue = "true", property = "tycho.apitools.verify.skipIfReplaced") private boolean skipIfReplaced; @@ -104,6 +102,12 @@ public class ApiAnalysisMojo extends AbstractMojo { @Parameter private Map properties; + @Parameter(defaultValue = "${project.basedir}/.settings/" + IApiCoreConstants.API_FILTERS_XML_NAME) + private File apiFilter; + + @Parameter(defaultValue = "${project.basedir}/.settings/org.eclipse.pde.api.tools.prefs") + private File apiPreferences; + @Component private EclipseWorkspaceManager workspaceManager; @@ -139,25 +143,33 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } long start = System.currentTimeMillis(); - Path targetFile; + Collection baselineBundles; try { - targetFile = createTargetFile(); + baselineBundles = getBaselineBundles(); } catch (DependencyResolutionException e) { getLog().warn("Can't resolve API baseline, API baseline check is skipped!"); return; - + } + Collection dependencyBundles; + try { + dependencyBundles = getProjectDependencies(); + } catch (Exception e) { + throw new MojoFailureException("Can't fetch dependencies!", e); } EclipseWorkspace workspace = getWorkspace(); - List configuration = setupArguments(targetFile); EclipseApplication apiApplication = applicationResolver.getApiApplication(workspace.getKey().repository); EclipseFramework eclipseFramework; try { - eclipseFramework = apiApplication.startFramework(workspace, configuration); + eclipseFramework = apiApplication.startFramework(workspace, List.of()); } catch (BundleException e) { throw new MojoFailureException("Start Framework failed!", e); } try { - eclipseFramework.start(); + eclipseFramework.execute(new ApiAnalysis(baselineBundles, dependencyBundles, project.getName(), + apiFilter == null ? null : apiFilter.toPath(), + apiPreferences == null ? null : apiPreferences.toPath(), + project.getArtifact().getFile().toPath(), + debug)); } catch (Exception e) { throw new MojoExecutionException("Execute ApiApplication failed", e); } finally { @@ -187,48 +199,22 @@ private MavenRepositoryLocation getRepository() { } - private List setupArguments(Path targetFile) - throws MojoFailureException { - List args = new ArrayList<>(); - args.add("-application"); - args.add("org.eclipse.pde.api.tools.apiAnalyzer"); - args.add("-project"); - args.add(project.getBasedir().getAbsolutePath()); - args.add("-baseline"); - args.add(targetFile.toAbsolutePath().toString()); - args.add("-dependencyList"); - try { - args.add(writeProjectDependencies().toAbsolutePath().toString()); - } catch (Exception e) { - throw new MojoFailureException("Can't write dependencies!", e); - } - args.add("-failOnError"); - return args; - } - - private Path createTargetFile() throws MojoExecutionException, MojoFailureException { + private Collection getBaselineBundles() throws MojoFailureException { long start = System.currentTimeMillis(); Collection baselineBundles; try { Optional artifactKey = projectManager.getArtifactKey(project); getLog().info("Resolve API baseline for " + project.getId()); - baselineBundles = resolver.getApiBaselineBundles(baselines.stream().filter(repo -> repo.getUrl() != null) - .map(repo -> new MavenRepositoryLocation(repo.getId(), URI.create(repo.getUrl()))).toList(), + baselineBundles = resolver.getApiBaselineBundles( + baselines.stream().filter(repo -> repo.getUrl() != null) + .map(repo -> new MavenRepositoryLocation(repo.getId(), URI.create(repo.getUrl()))).toList(), artifactKey.get()); getLog().debug("API baseline contains " + baselineBundles.size() + " bundles (resolve takes " + time(start) + ")."); } catch (IllegalArtifactReferenceException e) { throw new MojoFailureException("Project specify an invalid artifact key", e); } - String list = baselineBundles.stream().map(p -> p.toAbsolutePath().toString()) - .collect(Collectors.joining(System.lineSeparator())); - Path targetFile = Path.of(project.getBuild().getDirectory(), project.getArtifactId() + "-apiBaseline.txt"); - try { - Files.writeString(targetFile, list, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new MojoExecutionException("Writing target file failed!", e); - } - return targetFile; + return baselineBundles; } private String time(long start) { @@ -240,69 +226,54 @@ private String time(long start) { return sec + " s"; } - private Path writeProjectDependencies() throws Exception { - File outputFile = new File(project.getBuild().getDirectory(), "dependencies-list.txt"); - outputFile.getParentFile().mkdirs(); - Set written = new HashSet<>(); + private Collection getProjectDependencies() throws Exception { + Set dependencySet = new HashSet<>(); TychoProject tychoProject = projectManager.getTychoProject(project).get(); - try (BufferedWriter writer = Files.newBufferedWriter(outputFile.toPath())) { - List dependencies = TychoProjectUtils - .getDependencyArtifacts(DefaultReactorProject.adapt(project)).getArtifacts(); - for (ArtifactDescriptor descriptor : dependencies) { - File location = descriptor.fetchArtifact().get(); - if (location.equals(project.getBasedir())) { - continue; - } - ReactorProject reactorProject = descriptor.getMavenProject(); - if (reactorProject == null) { - writeLocation(writer, location, written); - } else { - ReactorProject otherProject = reactorProject; - writeLocation(writer, otherProject.getArtifact(descriptor.getClassifier()), written); - } + List dependencies = TychoProjectUtils + .getDependencyArtifacts(DefaultReactorProject.adapt(project)).getArtifacts(); + for (ArtifactDescriptor descriptor : dependencies) { + File location = descriptor.fetchArtifact().get(); + if (location.equals(project.getBasedir())) { + continue; + } + ReactorProject reactorProject = descriptor.getMavenProject(); + if (reactorProject == null) { + writeLocation(location, dependencySet); + } else { + ReactorProject otherProject = reactorProject; + writeLocation(otherProject.getArtifact(descriptor.getClassifier()), dependencySet); } - if (tychoProject instanceof OsgiBundleProject bundleProject) { - pluginRealmHelper.visitPluginExtensions(project, session, ClasspathContributor.class, cpc -> { - List list = cpc.getAdditionalClasspathEntries(project, Artifact.SCOPE_COMPILE); - if (list != null && !list.isEmpty()) { - for (ClasspathEntry entry : list) { - for (File locations : entry.getLocations()) { - try { - writeLocation(writer, locations, written); - } catch (IOException e) { - // ignore... - } - } + } + if (tychoProject instanceof OsgiBundleProject bundleProject) { + pluginRealmHelper.visitPluginExtensions(project, session, ClasspathContributor.class, cpc -> { + List list = cpc.getAdditionalClasspathEntries(project, Artifact.SCOPE_COMPILE); + if (list != null && !list.isEmpty()) { + for (ClasspathEntry entry : list) { + for (File locations : entry.getLocations()) { + writeLocation(locations, dependencySet); } } - }); - // This is a hack because "org.eclipse.osgi.services" exports the annotation - // package and might then be resolved by Tycho as a dependency, but then PDE - // can't find the annotations here, so we always add this as a dependency - // manually here, once "org.eclipse.osgi.services" is gone we can remove this - // again! - Optional bundle = mavenBundleResolver.resolveMavenBundle(project, session, - "org.osgi", "org.osgi.service.component.annotations", "1.3.0"); - bundle.ifPresent(key -> { - try { - writeLocation(writer, key.getLocation(), written); - } catch (IOException e) { - } - }); - } + } + }); + // This is a hack because "org.eclipse.osgi.services" exports the annotation + // package and might then be resolved by Tycho as a dependency, but then PDE + // can't find the annotations here, so we always add this as a dependency + // manually here, once "org.eclipse.osgi.services" is gone we can remove this + // again! + Optional bundle = mavenBundleResolver.resolveMavenBundle(project, session, "org.osgi", + "org.osgi.service.component.annotations", "1.3.0"); + bundle.ifPresent(key -> { + writeLocation(key.getLocation(), dependencySet); + }); } - return outputFile.toPath(); + return dependencySet; } - private void writeLocation(BufferedWriter writer, File location, Set written) throws IOException { + private void writeLocation(File location, Collection consumer) { if (location == null) { return; } - String path = location.getAbsolutePath(); - if (written.add(path)) { - writer.write(path); - writer.write(System.lineSeparator()); - } + consumer.add(location.getAbsoluteFile().toPath()); } private static final class ApiAppKey { diff --git a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisResult.java b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisResult.java new file mode 100644 index 0000000000..d254ac2dda --- /dev/null +++ b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisResult.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2023 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.apitools; + +import java.io.Serializable; + +public class ApiAnalysisResult implements Serializable { + +}