dev.equo.ide.BuildPluginIdeMain Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2022-2023 EquoTech, 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:
* EquoTech, Inc. - initial API and implementation
*******************************************************************************/
package dev.equo.ide;
import com.diffplug.common.swt.os.OS;
import dev.equo.solstice.NestedJars;
import dev.equo.solstice.SerializableMisc;
import dev.equo.solstice.ShimIdeBootstrapServices;
import dev.equo.solstice.SignedJars;
import dev.equo.solstice.Solstice;
import dev.equo.solstice.SolsticeManifest;
import dev.equo.solstice.p2.WorkspaceRegistry;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import org.eclipse.osgi.internal.location.EquinoxLocations;
import org.eclipse.swt.widgets.Display;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
/**
* A main method for launching an IDE using Solstice. It has a verbose command line interface which
* is optimized for integration with the EquoIDE Gradle and Maven build plugins, but it can be used
* in other contexts as well.
*/
public class BuildPluginIdeMain {
public static class Caller {
public static Caller forProjectDir(File projectDir, boolean clean)
throws IOException, InterruptedException {
var caller = new Caller();
var workspaceRegistry = WorkspaceRegistry.instance();
caller.workspaceDir = workspaceRegistry.workspaceDirForProjectDir(projectDir);
workspaceRegistry.removeAbandoned();
caller.lockFile = IdeLockFile.forWorkspaceDir(caller.workspaceDir);
var alreadyRunning = caller.lockFile.ideAlreadyRunning();
if (IdeLockFile.alreadyRunningAndUserRequestsAbort(alreadyRunning)) {
return null;
}
if (clean) {
workspaceRegistry.cleanWorkspaceDir(caller.workspaceDir);
}
return caller;
}
private Caller() {}
public File workspaceDir;
public IdeLockFile lockFile;
public IdeHook.List ideHooks;
public WorkspaceInit workspaceInit;
public ArrayList classpath;
public BuildPluginIdeMain.DebugClasspath debugClasspath;
public Boolean initOnly, showConsole, useAtomos, debugIde;
public String showConsoleFlag, cleanFlag;
public void launch() throws IOException, InterruptedException {
Objects.requireNonNull(workspaceDir);
Objects.requireNonNull(lockFile);
Objects.requireNonNull(ideHooks);
Objects.requireNonNull(workspaceInit);
Objects.requireNonNull(classpath);
Objects.requireNonNull(debugClasspath);
Objects.requireNonNull(initOnly);
Objects.requireNonNull(showConsole);
Objects.requireNonNull(useAtomos);
Objects.requireNonNull(debugIde);
Objects.requireNonNull(showConsoleFlag);
Objects.requireNonNull(cleanFlag);
ArrayList classpathSorted = Launcher.copyAndSortClasspath(classpath);
SignedJars.stripIfNecessary(classpathSorted);
var nestedJarFolder = new File(workspaceDir, NestedJars.DIR);
for (var nested : NestedJars.inFiles(classpathSorted).extractAllNestedJars(nestedJarFolder)) {
classpathSorted.add(nested.getValue());
}
var vmArgs = new ArrayList();
var environmentVars = new LinkedHashMap();
if (Catalog.EQUO_CHROMIUM.isEnabled(classpath)) {
List chromiumArgs = new ArrayList();
// This property is used to fix the error in the setUrl method.
chromiumArgs.add("--disable-site-isolation-trials");
// This property is used to fix the error when logging in with a third party.
chromiumArgs.add("--user-agent=" + EquoChromium.getUserAgent());
vmArgs.add("-Dchromium.args=" + String.join(";", chromiumArgs));
// This property improve loading time of setText for large resources.
vmArgs.add("-Dchromium.setTextAsUrl=file:");
// Fix graphics on linux
if (OS.getRunning().isLinux()) {
environmentVars.put("GDK_BACKEND", "x11");
}
// Patch in our browser replacement, which requires stripping the signature from the SWT
// packages, and activate the license
Patch.patch(classpathSorted, nestedJarFolder, "patch-chromium-swt");
SignedJars.stripIf(classpathSorted, fileName -> fileName.startsWith("org.eclipse.swt."));
vmArgs.add(
"-Dchromium.activate_equo_chromium=This distribution of the Equo browser is licensed only for use with an IDE launched by the EquoIDE build plugin");
}
if (useAtomos) {
// for Eclipse 4.27, we have patched the EquinoxBundle class so that it handles
// `getEntry("/")`
var version = Patch.detectVersion(classpathSorted, "org.eclipse.osgi");
if ("3.18.300".equals(version)) {
Patch.patch(classpathSorted, nestedJarFolder, "patch-equinox-4.27");
SignedJars.stripIf(classpathSorted, jarName -> jarName.startsWith("org.eclipse.osgi"));
}
}
SignedJars.stripIfNecessary(classpathSorted);
if (lockFile.hasClasspath() && !classpathSorted.equals(lockFile.readClasspath())) {
System.out.println("WARNING! The classpath has changed since this IDE was setup.");
System.out.println(
" Recommend closing the IDE and retrying with this flag: " + cleanFlag);
}
var ideHooksFile = new File(workspaceDir, "ide-hooks");
var ideHooksCopy = ideHooks.copy();
// add any IdeHooks which were declared in jar manifests
for (var jar : classpathSorted) {
var ideHook = SolsticeManifest.parseJar(jar).getHeadersOriginal().get("Bundle-IdeHook");
if (ideHook != null) {
ideHooksCopy.add(new IdeHookReflected(ideHook));
}
}
ideHooksCopy.add(IdeHookLockFile.forWorkspaceDirAndClasspath(workspaceDir, classpathSorted));
SerializableMisc.toFile(ideHooksCopy, ideHooksFile);
workspaceInit.applyTo(workspaceDir);
var installDir = workspaceDir.toPath().resolve("install");
Files.createDirectories(installDir);
var bundlesInfo =
workspaceDir
.toPath()
.resolve("config/org.eclipse.equinox.simpleconfigurator/bundles.info");
Files.createDirectories(bundlesInfo.getParent());
Files.writeString(bundlesInfo, bundlesDotInfo(classpathSorted));
debugClasspath.printWithHead(
"jars about to be launched", classpathSorted.stream().map(File::getAbsolutePath));
boolean isBlocking =
initOnly
|| showConsole
|| debugClasspath != BuildPluginIdeMain.DebugClasspath.disabled
|| debugIde;
if (OS.getRunning().isMac()) {
vmArgs.add("-XstartOnFirstThread");
}
vmArgs.add("-Dorg.slf4j.simpleLogger.defaultLogLevel=" + (isBlocking ? "info" : "error"));
if (debugIde) {
vmArgs.add("-Xdebug");
vmArgs.add("-Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y");
System.out.println("IDE will block until you attach a jdb debugger to port 8000");
System.out.println(" e.g. jdb -attach localhost:8000");
}
Consumer monitorProcess;
if (isBlocking) {
monitorProcess = null;
} else {
// if we're spawning a new IDE, record the lockfile before it launches
long lockFileBeforeLaunch = lockFile.readPidToken();
monitorProcess =
process -> {
// sleep over and over until the lockfile changes
while (lockFile.readPidToken() == lockFileBeforeLaunch) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// ignore
}
}
// kill the console that we've been waiting on as a solution to
// https://github.com/equodev/equo-ide/issues/44
process.destroyForcibly();
};
}
var exitCode =
Launcher.launchJavaBlocking(
isBlocking,
classpathSorted,
vmArgs,
BuildPluginIdeMain.class.getName(),
monitorProcess,
"-installDir",
workspaceDir.getAbsolutePath(),
"-useAtomos",
Boolean.toString(useAtomos),
"-initOnly",
Boolean.toString(initOnly),
"-debugClasspath",
debugClasspath.name(),
"-ideHooks",
ideHooksFile.getAbsolutePath());
if (!isBlocking) {
System.out.println("NEED HELP? If the IDE doesn't appear, try adding " + showConsoleFlag);
}
if (exitCode != 0) {
System.out.println("WARNING! Exit code: " + exitCode);
}
}
}
public enum DebugClasspath {
disabled,
names,
paths;
public void printWithHead(String header, Stream paths) {
switch (this) {
case disabled:
return;
case names:
case paths:
System.out.println("/ " + header);
if (this == names) {
paths =
paths.map(
path -> {
int lastSlash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
return path.substring(lastSlash + 1);
});
}
paths.forEach(System.out::println);
System.out.println("\\ " + header);
return;
default:
throw new IllegalArgumentException("Unexpected enum value " + this);
}
}
private void printAndExitIfEnabled() throws IOException {
switch (this) {
case disabled:
return;
case names:
case paths:
Enumeration manifestURLs =
SolsticeManifest.class.getClassLoader().getResources(SolsticeManifest.MANIFEST_PATH);
List paths = new ArrayList<>();
while (manifestURLs.hasMoreElements()) {
String url = manifestURLs.nextElement().toExternalForm();
paths.add(
url.substring(
0, url.length() - (SolsticeManifest.SLASH_MANIFEST_PATH.length() + 1)));
}
printWithHead("jars with manifests inside runtime", paths.stream());
System.exit(0);
default:
throw new IllegalArgumentException("Unexpected enum value " + this);
}
}
}
private static String bundlesDotInfo(List cp) {
var buffer = new StringBuilder();
var newline = "\n";
// for a "real" file these should be different in different places...
var startLevel = "0";
var markedAsStarted = "false";
buffer.append("#encoding=UTF-8");
buffer.append(newline);
buffer.append("#version=1");
buffer.append(newline);
for (var file : cp) {
try {
SolsticeManifest manifest = SolsticeManifest.parseJar(file);
if (manifest == null
|| manifest.getSymbolicName() == null
|| manifest.getVersion() == null) {
continue;
}
buffer.append(manifest.getSymbolicName());
buffer.append(',');
buffer.append(manifest.getVersion());
buffer.append(',');
buffer.append(file.toURI());
buffer.append(',');
buffer.append(startLevel);
buffer.append(',');
buffer.append(markedAsStarted);
buffer.append(newline);
} catch (Exception e) {
// do nothing
}
}
return buffer.toString();
}
private static T parseArg(
String[] args, String arg, Function parser, T defaultValue) {
for (int i = 0; i < args.length - 1; ++i) {
if (arg.equals(args[i])) {
return parser.apply(args[i + 1]);
}
}
return defaultValue;
}
static File defaultDir() {
var userDir = System.getProperty("user.dir");
if (userDir.endsWith("equo-ide")) {
return new File(userDir + "/solstice/build/testSetup");
} else {
return new File(userDir + "/build/testSetup");
}
}
public static void main(String[] args)
throws InvalidSyntaxException, BundleException, IOException {
File installDir = parseArg(args, "-installDir", File::new, defaultDir());
boolean useAtomos = parseArg(args, "-useAtomos", Boolean::parseBoolean, false);
boolean initOnly = parseArg(args, "-initOnly", Boolean::parseBoolean, false);
DebugClasspath debugClasspath =
parseArg(args, "-debugClasspath", DebugClasspath::valueOf, DebugClasspath.disabled);
File hookListFile = parseArg(args, "-ideHooks", File::new, null);
IdeHook.List ideHooksParsed;
if (hookListFile == null) {
ideHooksParsed = new IdeHook.List();
} else {
ideHooksParsed = SerializableMisc.fromFile(IdeHook.List.class, hookListFile);
}
debugClasspath.printAndExitIfEnabled();
NestedJars.onClassPath()
.confirmAllNestedJarsArePresentOnClasspath(new File(installDir, NestedJars.DIR));
var solstice = Solstice.findBundlesOnClasspath();
solstice.warnAndModifyManifestsToFix();
IdeHook.InstantiatedList ideHooks = ideHooksParsed.instantiate();
var lockFileHook = ideHooks.find(IdeHookLockFile.Instantiated.class);
boolean isClean = lockFileHook == null || lockFileHook.isClean();
if (!initOnly) {
ideHooks.forEach(IdeHookInstantiated::isClean, isClean);
var display = Display.getDefault();
ideHooks.forEach(IdeHookInstantiated::afterDisplay, display);
}
var props = new LinkedHashMap();
props.put(
"org.eclipse.equinox.simpleconfigurator.configUrl",
"file: broken on purpose to disable simpleconfigurator");
props.put("gosh.args", "--quiet --noshutdown");
props.put("osgi.nl", "en_US");
props.put("eclipse.noRegistryFlushing", "true");
props.put(Constants.FRAMEWORK_STORAGE_CLEAN, Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
props.put(
EquinoxLocations.PROP_INSTANCE_AREA, new File(installDir, "instance").getAbsolutePath());
props.put(
EquinoxLocations.PROP_INSTALL_AREA, new File(installDir, "install").getAbsolutePath());
props.put(EquinoxLocations.PROP_CONFIG_AREA, new File(installDir, "config").getAbsolutePath());
props.put(EquinoxLocations.PROP_USER_AREA, new File(installDir, "user").getAbsolutePath());
File eclipseHome = new File(installDir, "eclipse-home");
props.put(EquinoxLocations.PROP_HOME_LOCATION_AREA, eclipseHome.getAbsolutePath());
System.setProperty(EquinoxLocations.PROP_HOME_LOCATION_AREA, eclipseHome.toURI().toString());
if (useAtomos) {
props.put("atomos.content.start", "false");
solstice.openAtomos(props);
} else {
solstice.openShim(props);
ShimIdeBootstrapServices.apply(props, solstice.getContext());
}
ShimIdeBootstrapServices.shimAndAtomos(props, solstice.getContext());
solstice.start("org.apache.felix.scr");
solstice.startAllWithLazy(false);
for (var eagerStart :
solstice.bundlesOnClasspathOutOf(Fudge.activateEagerWithoutTransitives())) {
solstice.startWithoutTransitives(eagerStart);
}
solstice.start("org.eclipse.ui.ide.application");
if (useAtomos) {
// the spelled-out package is on purpose so that Atomos can remain an optional component
// works together with
// https://github.com/equodev/equo-ide/blob/aa7d30cba9988bc740ff4bc4b3015475d30d187c/solstice/build.gradle#L16-L22
dev.equo.solstice.BundleContextAtomos.urlWorkaround(solstice);
}
if (!initOnly) {
ideHooks.forEach(IdeHookInstantiated::afterOsgi, solstice.getContext());
}
if (initOnly) {
System.out.println(
"Loaded "
+ solstice.getContext().getBundles().length
+ " bundles "
+ (useAtomos ? "using Atomos" : "not using Atomos"));
System.exit(0);
return;
}
int exitCode = IdeMainUi.main(solstice, ideHooks);
if (exitCode == 0) {
System.exit(0);
} else {
System.err.println("Unexpected exit code: " + exitCode);
System.exit(1);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy