blob: d1744fb1ded7b7922efee7b8058f232db1fc4ea2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2019 Red Hat Inc. 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:
* - Mickael Istria (Red Hat Inc.)
*******************************************************************************/
package org.eclipse.pde.api.tools.internal;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceDescription;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.equinox.app.IApplication;
import org.eclipse.equinox.app.IApplicationContext;
import org.eclipse.equinox.frameworkadmin.BundleInfo;
import org.eclipse.pde.api.tools.internal.model.ApiBaseline;
import org.eclipse.pde.api.tools.internal.model.BundleComponent;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
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.IApiProblemTypes;
import org.eclipse.pde.core.target.ITargetDefinition;
import org.eclipse.pde.core.target.ITargetLocation;
import org.eclipse.pde.core.target.ITargetPlatformService;
import org.eclipse.pde.core.target.LoadTargetDefinitionJob;
import org.eclipse.pde.core.target.TargetBundle;
import org.eclipse.pde.internal.core.ICoreConstants;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.core.target.TargetPlatformService;
import org.osgi.framework.Bundle;
public class ApiAnalysisApplication implements IApplication {
private static class Request {
private static final String FAIL_ON_ERROR_ARG = "failOnError"; //$NON-NLS-1$
private static final String PROJECT_ARG = "project"; //$NON-NLS-1$
private static final String BASELINE_ARG = "baseline"; //$NON-NLS-1$
private static final String BASELINE_DEFAULT_VALUE = "default"; //$NON-NLS-1$
private static final String DEPENDENCY_LIST_ARG = "dependencyList"; //$NON-NLS-1$
private Request() {
}
public static Request readFromArgs(String[] params) {
Request res = new Request();
String currentKey = null;
for (String param : params) {
if (param.charAt(0) == '-') {
if (FAIL_ON_ERROR_ARG.equals(currentKey)) {
res.failOnError = true;
}
currentKey = param.substring(1);
} else if (PROJECT_ARG.equals(currentKey)) {
res.project = new File(param);
} else if (BASELINE_ARG.equals(currentKey) && !BASELINE_DEFAULT_VALUE.equals(param)) {
res.baselinePath = new File(param);
} else if (FAIL_ON_ERROR_ARG.equals(currentKey)) {
res.failOnError = Boolean.parseBoolean(param);
} else if (DEPENDENCY_LIST_ARG.equals(currentKey)) {
res.tpFile = new File(param);
}
}
if (FAIL_ON_ERROR_ARG.equals(currentKey)) {
res.failOnError = true;
}
return res;
}
public File project;
public File baselinePath;
public boolean failOnError;
public File tpFile;
}
@Override
public Object start(IApplicationContext context) throws Exception {
try {
IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription();
desc.setAutoBuilding(false);
ResourcesPlugin.getWorkspace().setDescription(desc);
PDECore.getDefault().getPreferencesManager().setValue(ICoreConstants.DISABLE_API_ANALYSIS_BUILDER, false);
Request args = Request
.readFromArgs((String[]) context.getArguments().get(IApplicationContext.APPLICATION_ARGS));
IProject project = importProject(args.project);
if (project == null) {
System.err.println("Project not loaded."); //$NON-NLS-1$
return IStatus.ERROR;
}
IApiBaseline baseline = setBaseline(args.baselinePath);
if (baseline == null) {
System.err.println("Baseline shouldn't be null."); //$NON-NLS-1$
return IStatus.ERROR;
}
setTargetPlatform(args.tpFile);
configureSeverity(project);
project.build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
IMarker[] allProblemMarkers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
Predicate<IMarker> isAPIMarker = marker -> {
try {
return marker.getType().startsWith(ApiPlugin.PLUGIN_ID);
} catch (CoreException e) {
ApiPlugin.log(e);
return false;
}
};
IMarker[] allAPIProbleMarkers = Arrays.stream(allProblemMarkers) //
.filter(isAPIMarker) //
.toArray(IMarker[]::new);
IMarker[] allNonAPIErrors = Arrays.stream(allProblemMarkers) //
.filter(isAPIMarker.negate()) //
.filter(marker -> marker.getAttribute(IMarker.SEVERITY, -1) == IMarker.SEVERITY_ERROR) //
.toArray(IMarker[]::new);
if (allNonAPIErrors.length > 0) {
System.err.println("Some blocking (most likely link/compilation) errors are present:"); //$NON-NLS-1$
for (IMarker marker : allNonAPIErrors) {
System.err.println("* " + marker); //$NON-NLS-1$
}
System.err.println("Some blocking (most likely link/compilation) errors are present ^^^"); //$NON-NLS-1$
return 10;
}
// errors
IMarker[] errorMarkers = Arrays.stream(allAPIProbleMarkers)
.filter(marker -> marker.getAttribute(IMarker.SEVERITY, -1) == IMarker.SEVERITY_ERROR)
.toArray(IMarker[]::new);
System.err.println(errorMarkers.length + " API ERRORS"); //$NON-NLS-1$
for (IMarker marker : errorMarkers) {
System.err.println("* " + marker); //$NON-NLS-1$
}
// warnings
IMarker[] warningMarkers = Arrays.stream(allAPIProbleMarkers)
.filter(marker -> marker.getAttribute(IMarker.SEVERITY, -1) == IMarker.SEVERITY_WARNING)
.toArray(IMarker[]::new);
System.out.println(warningMarkers.length + " API warnings"); //$NON-NLS-1$
for (IMarker marker : warningMarkers) {
System.out.println("* " + marker); //$NON-NLS-1$
}
// fail
if (args.failOnError && errorMarkers.length > 0) {
return IStatus.ERROR;
}
return IStatus.OK;
} catch (Exception e) {
e.printStackTrace();
return IStatus.ERROR;
}
}
private void setTargetPlatform(File dependencyList) throws IOException, CoreException, InterruptedException {
if (dependencyList != null) {
if (!(dependencyList.isFile() && dependencyList.canRead())) {
throw new IllegalArgumentException("dependencyList argument points to non radable file: " + dependencyList.getAbsolutePath());//$NON-NLS-1$
}
// File is typically the output of `mvn dependnecy:list
// -DoutputAbsoluteArtifactFilename=true -DoutputScope=false -DoutputFile=...`
// like
// ```
// The following files have been resolved:
// p2.eclipse-plugin:org.eclipse.equinoxz.event:jar:1.5.0.v20181008-1938:/home/mistria/.m2/repository/p2/osgi/bundle/org.eclipse.equinox.event/1.5.0.v20181008-1938/org.eclipse.equinox.event-1.5.0.v20181008-1938.jar
// p2.eclipse-plugin:org.eclipse.equinox.p2.core:jar:2.6.0.v20190215-2242:/home/mistria/.m2/repository/p2/osgi/bundle/org.eclipse.equinox.p2.core/2.6.0.v20190215-2242/org.eclipse.equinox.p2.core-2.6.0.v20190215-2242.jar
// p2.eclipse-plugin:org.eclipse.osgi.compatibility.state:jar:1.1.400.v20190208-1533:/home/mistria/.m2/repository/p2/osgi/bundle/org.eclipse.osgi.compatibility.state/1.1.400.v20190208-1533/org.eclipse.osgi.compatibility.state-1.1.400.v20190208-1533.jar
// ```
ITargetPlatformService service = TargetPlatformService.getDefault();
ITargetDefinition target = service.newTarget();
target.setName("buildpath"); //$NON-NLS-1$
TargetBundle[] bundles = Files.readAllLines(dependencyList.toPath()).stream()//
.filter(line -> line.contains("jar")) //$NON-NLS-1$
.flatMap(line -> Arrays.stream(line.split(":"))) //$NON-NLS-1$
.filter(maybePath -> !maybePath.trim().isEmpty())
.map(File::new)//
.filter(File::isAbsolute)//
.filter(File::isFile)//
.map(absoluteFile -> {
try {
return new TargetBundle(absoluteFile);
} catch (CoreException e) {
ApiPlugin.log(e);
return null;
}
}).filter(Objects::nonNull)//
.toArray(TargetBundle[]::new);
target.setTargetLocations(new ITargetLocation[] { new BundleListTargetLocation(bundles) });
service.saveTargetDefinition(target);
Job job = new LoadTargetDefinitionJob(target);
job.schedule();
job.join();
}
}
protected void configureSeverity(IProject project) {
Map<String, String> enforcedSeverities = new HashMap<>();
enforcedSeverities.put(IApiProblemTypes.INCOMPATIBLE_API_COMPONENT_VERSION_REPORT_MAJOR_WITHOUT_BREAKING_CHANGE,
ApiPlugin.VALUE_ERROR);
enforcedSeverities.put(IApiProblemTypes.INCOMPATIBLE_API_COMPONENT_VERSION_REPORT_MINOR_WITHOUT_API_CHANGE,
ApiPlugin.VALUE_ERROR);
IEclipsePreferences projectNode = new ProjectScope(project).getNode(ApiPlugin.PLUGIN_ID);
enforcedSeverities.forEach((key, value) -> {
PDECore.getDefault().getPreferencesManager().setValue(key, value);
projectNode.put(key, value);
});
}
private IApiBaseline setBaseline(File baselinePath) throws CoreException, IOException {
if (baselinePath == null) {
ApiBaseline baseline = new ApiBaseline("current running application"); //$NON-NLS-1$
for (Bundle bundle : ApiPlugin.getDefault().getBundle().getBundleContext().getBundles()) {
if (bundle.getBundleId() != 0) {
baseline.addApiComponents(
new IApiComponent[] { new BundleComponent(baseline,
FileLocator.getBundleFile(bundle).getAbsolutePath(), bundle.getBundleId()) });
}
}
ApiBaselineManager.getManager().addApiBaseline(baseline);
ApiBaselineManager.getManager().setDefaultApiBaseline(baseline.getName());
return baseline;
} else if (baselinePath.isFile() && baselinePath.getName().endsWith(".target")) { //$NON-NLS-1$
ITargetPlatformService service = TargetPlatformService.getDefault();
ITargetDefinition definition = service.getTarget(baselinePath.toURI()).getTargetDefinition();
IStatus resolutionStatus = definition.resolve(new NullProgressMonitor());
switch (resolutionStatus.getSeverity())
{
case IStatus.WARNING:
System.out.println("WARNING resolving target platform: " + resolutionStatus.getMessage()); //$NON-NLS-1$
break;
case IStatus.ERROR:
throw new CoreException(resolutionStatus);
default: // Nothing
}
ApiBaseline baseline = new ApiBaseline(baselinePath.getAbsolutePath());
for (TargetBundle bundle : definition.getAllBundles()) {
BundleInfo bundleInfo = bundle.getBundleInfo();
if (bundleInfo.getBundleId() != 0) {
baseline.addApiComponents(new IApiComponent[] { new BundleComponent(baseline,
new File(bundleInfo.getLocation()).getAbsolutePath(), bundleInfo.getBundleId()) });
}
}
ApiBaselineManager.getManager().addApiBaseline(baseline);
ApiBaselineManager.getManager().setDefaultApiBaseline(baseline.getName());
return baseline;
} else if (baselinePath.isDirectory()) {
System.err.println(
"Support for directories not implemented yet, use `default` or a `</path/to/baseline.target>` baseline for currently running application."); //$NON-NLS-1$
return null;
}
return ApiBaselineManager.getManager().getDefaultApiBaseline();
}
private IProject importProject(File projectPath) throws CoreException {
File dotProject = new File(projectPath, IProjectDescription.DESCRIPTION_FILE_NAME);
if (!dotProject.isFile()) {
System.err.println("Expected `" + dotProject.getAbsolutePath() + "` file doesn't exist."); //$NON-NLS-1$ //$NON-NLS-2$
return null;
}
IProjectDescription projectDescription = ResourcesPlugin.getWorkspace()
.loadProjectDescription(Path.fromOSString(dotProject.getAbsolutePath()));
projectDescription.setLocation(Path.fromOSString(projectPath.getAbsolutePath()));
IProject res = ResourcesPlugin.getWorkspace().getRoot().getProject(projectDescription.getName());
if (res.exists()) {
if (!res.getDescription().getLocationURI().equals(projectDescription.getLocationURI())) {
System.err.println("Project with same name and different location exists in workspace."); //$NON-NLS-1$
return null;
}
res.open(new NullProgressMonitor());
res.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
} else {
res.create(projectDescription, new NullProgressMonitor());
res.open(new NullProgressMonitor());
}
projectDescription = res.getDescription();
if (Arrays.stream(res.getDescription().getBuildSpec()).map(ICommand::getBuilderName)
.noneMatch(ApiPlugin.BUILDER_ID::equals)) {
ICommand[] builders = new ICommand[projectDescription.getBuildSpec().length + 1];
System.arraycopy(projectDescription.getBuildSpec(), 0, builders, 0,
projectDescription.getBuildSpec().length);
ICommand buildCommand = projectDescription.newCommand();
buildCommand.setBuilderName(ApiPlugin.BUILDER_ID);
builders[builders.length - 1] = buildCommand;
projectDescription.setBuildSpec(builders);
res.setDescription(projectDescription, 0, new NullProgressMonitor());
}
ICommand[] buildSpec = projectDescription.getBuildSpec();
List<ICommand> newBuilders = removeManifestAndSchemaBuilders(buildSpec);
projectDescription.setBuildSpec(
newBuilders.toArray(new ICommand[newBuilders.size()]));
res.setDescription(projectDescription, new NullProgressMonitor());
return res;
}
private static List<ICommand> removeManifestAndSchemaBuilders(ICommand[] buildSpec) {
// remove manifest and schema builders
return Arrays.stream(buildSpec)
.filter(x -> !("org.eclipse.pde.ManifestBuilder".equals(x.getBuilderName()) //$NON-NLS-1$
|| "org.eclipse.pde.SchemaBuilder".equals(x.getBuilderName())) //$NON-NLS-1$
).collect(Collectors.toList());
}
@Override
public void stop() {
// Nothing to do
}
}