From 345db752a394c4506f41462dc05c217c315254fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Sat, 22 Mar 2025 07:39:29 +0100 Subject: [PATCH] Add support for creating workspace private project descriptions Currently importing a project into eclipse requires the creation of a physical file name .project in the root of the project. this has several drawbacks for tools that automatically discover projects and import them for the user as it creates new files and possibly dirty their working tree. Other tools use a single folder for this purpose or don't require any permanent files in the working-tree itself. Even Eclipse has already such concept that is used when a project is located outside the workspace. This now adds a new feature called "workspace private project" that only holds the basic information in the location file in the workspace directory, any additional information might be needed to restore by a tool that uses workspace private projects. --- .../META-INF/MANIFEST.MF | 2 +- .../localstore/FileSystemResourceManager.java | 8 +- .../internal/resources/LocalMetaArea.java | 93 +++++++++++++++++-- .../internal/resources/ModelObjectWriter.java | 4 +- .../core/internal/resources/Project.java | 2 +- .../resources/ProjectDescription.java | 20 ++++ .../resources/ProjectDescriptionReader.java | 41 ++++---- .../core/internal/resources/SaveManager.java | 3 + .../core/resources/IProjectDescription.java | 15 +++ 9 files changed, 157 insertions(+), 31 deletions(-) diff --git a/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF b/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF index aa042e2f420..919bb4d52db 100644 --- a/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF +++ b/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.core.resources; singleton:=true -Bundle-Version: 3.22.200.qualifier +Bundle-Version: 3.23.0.qualifier Bundle-Activator: org.eclipse.core.resources.ResourcesPlugin Bundle-Vendor: %providerName Bundle-Localization: plugin diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java index 2b200644e34..9ba5f0b5590 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java @@ -683,7 +683,7 @@ public boolean internalWrite(IProject target, IProjectDescription description, i if (hasPrivateChanges) getWorkspace().getMetaArea().writePrivateDescription(target); //can't do anything if there's no description - if (!hasPublicChanges || (description == null)) + if (!hasPublicChanges || (description == null) || description.isWorkspacePrivate()) return false; //write the model to a byte array @@ -938,9 +938,15 @@ public ProjectDescription read(IProject target, boolean creation) throws CoreExc if (creation) { privateDescription = new ProjectDescription(); getWorkspace().getMetaArea().readPrivateDescription(target, privateDescription); + if (privateDescription.isWorkspacePrivate()) { + return privateDescription; + } projectLocation = privateDescription.getLocationURI(); } else { IProjectDescription description = ((Project) target).internalGetDescription(); + if (description instanceof ProjectDescription impl && impl.isWorkspacePrivate()) { + return impl; + } if (description != null && description.getLocationURI() != null) { projectLocation = description.getLocationURI(); } diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/LocalMetaArea.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/LocalMetaArea.java index 7ea32c18c11..bb3d9841d44 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/LocalMetaArea.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/LocalMetaArea.java @@ -24,13 +24,17 @@ import java.io.IOException; import java.net.URI; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Map.Entry; import org.eclipse.core.filesystem.URIUtil; +import org.eclipse.core.internal.events.BuildCommand; import org.eclipse.core.internal.localstore.SafeChunkyInputStream; import org.eclipse.core.internal.localstore.SafeChunkyOutputStream; import org.eclipse.core.internal.utils.Messages; import org.eclipse.core.internal.utils.Policy; import org.eclipse.core.resources.IBuildConfiguration; +import org.eclipse.core.resources.ICommand; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceStatus; @@ -322,14 +326,15 @@ public ProjectDescription readOldDescription(IProject project) throws CoreExcept } /** - * Returns the portions of the project description that are private, and - * adds them to the supplied project description. In particular, the - * project location, the project's dynamic references and build configurations - * are stored here. - * The project location will be set to null if the default - * location should be used. In the case of failure, log the exception and - * return silently, thus reverting to using the default location and no + * Returns the portions of the project description that are private, and adds + * them to the supplied project description. In particular, the project + * location, the project's dynamic references and build configurations are + * stored here. The project location will be set to null if the + * default location should be used. In the case of failure, log the exception + * and return silently, thus reverting to using the default location and no * dynamic references. The current format of the location file is: + * + *
 	 *    UTF - project location
 	 *    int - number of dynamic project references
 	 *    UTF - project reference 1
@@ -347,9 +352,24 @@ public ProjectDescription readOldDescription(IProject project) throws CoreExcept
 	 *        UTF - configName if hasConfigName
 	 *        ... repeat for number of referenced configurations
 	 *      ... repeat for number of build configurations with references
+	 * since 3.23:
+	 *    bool - private flag if project should only be read from its private project configuration
+	 *    int - number of natures
+	 *      UTF - nature id
+	 *      ... repeated for N natures
+	 *    int - number of buildspecs
+	 *      byte - type of buildspec
+	 *        (type 1) UTF - name of builder
+	 *                 int - number of arguments
+	 *                  UTF arg key
+	 *                  UTF arg value
+	 *                 UTF - triggers string
+	 * 
*/ public void readPrivateDescription(IProject target, ProjectDescription description) { IPath locationFile = locationFor(target).append(F_PROJECT_LOCATION); + String name = target.getName(); + description.setName(name); java.io.File file = locationFile.toFile(); if (!file.exists()) { locationFile = getBackupLocationFor(locationFile); @@ -370,7 +390,7 @@ public void readPrivateDescription(IProject target, ProjectDescription descripti } } catch (Exception e) { //don't allow failure to read the location to propagate - String msg = NLS.bind(Messages.resources_exReadProjectLocation, target.getName()); + String msg = NLS.bind(Messages.resources_exReadProjectLocation, name); Policy.log(new ResourceStatus(IStatus.ERROR, IResourceStatus.FAILED_READ_METADATA, target.getFullPath(), msg, e)); } //try to read the dynamic references - will fail for old location files @@ -408,6 +428,34 @@ public void readPrivateDescription(IProject target, ProjectDescription descripti m.put(configName, refs); } description.setBuildConfigReferences(m); + // read parts since 3.23 + description.setWorkspacePrivate(dataIn.readBoolean()); + String[] natureIds = new String[dataIn.readInt()]; + for (int i = 0; i < natureIds.length; i++) { + natureIds[i] = dataIn.readUTF(); + } + description.setNatureIds(natureIds); + int buildspecs = dataIn.readInt(); + ICommand[] buildSpecData = new ICommand[buildspecs]; + for (int i = 0; i < buildSpecData.length; i++) { + BuildCommand command = new BuildCommand(); + buildSpecData[i] = command; + int type = dataIn.read(); + if (type == 1) { + command.setName(dataIn.readUTF()); + int args = dataIn.readInt(); + Map map = new LinkedHashMap<>(); + for (int j = 0; j < args; j++) { + map.put(dataIn.readUTF(), dataIn.readUTF()); + } + command.setArguments(map); + String trigger = dataIn.readUTF(); + if (!trigger.isEmpty()) { + ProjectDescriptionReader.parseBuildTriggers(command, trigger); + } + } + description.setBuildSpec(buildSpecData); + } } catch (IOException e) { //ignore - this is an old location file or an exception occurred // closing the stream @@ -470,6 +518,35 @@ public void writePrivateDescription(IProject target) throws CoreException { } } } + // write parts since 3.23 + dataOut.writeBoolean(desc.isWorkspacePrivate()); + String[] natureIds = desc.getNatureIds(); + dataOut.writeInt(natureIds.length); + for (String id : natureIds) { + dataOut.writeUTF(id); + } + ICommand[] buildSpec = desc.getBuildSpec(false); + dataOut.write(buildSpec.length); + for (ICommand command : buildSpec) { + if (command instanceof BuildCommand b) { + dataOut.write(1); + dataOut.writeUTF(b.getName()); + Map arguments = b.getArguments(); + dataOut.writeInt(arguments.size()); + for (Entry entry : arguments.entrySet()) { + dataOut.writeUTF(entry.getKey()); + dataOut.writeUTF(entry.getValue()); + } + if (ModelObjectWriter.shouldWriteTriggers(b)) { + dataOut.writeUTF(ModelObjectWriter.triggerString(b)); + } else { + dataOut.writeUTF(""); //$NON-NLS-1$ + } + } else { + dataOut.write(0); + } + } + dataOut.flush(); output.succeed(); } catch (IOException e) { String message = NLS.bind(Messages.resources_exSaveProjectLocation, target.getName()); diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ModelObjectWriter.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ModelObjectWriter.java index 697e64a2f4a..43e113667d5 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ModelObjectWriter.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ModelObjectWriter.java @@ -44,7 +44,7 @@ public class ModelObjectWriter implements IModelObjectConstants { * Returns the string representing the serialized set of build triggers for * the given command */ - private static String triggerString(BuildCommand command) { + static String triggerString(BuildCommand command) { StringBuilder buf = new StringBuilder(); if (command.isBuilding(IncrementalProjectBuilder.AUTO_BUILD)) buf.append(TRIGGER_AUTO).append(','); @@ -83,7 +83,7 @@ protected void write(BuildCommand command, XMLWriter writer) { /** * Returns whether the build triggers for this command should be written. */ - private boolean shouldWriteTriggers(BuildCommand command) { + static boolean shouldWriteTriggers(BuildCommand command) { //only write triggers if command is configurable and there exists a trigger //that the builder does NOT respond to. I.e., don't write out on the default //cases to avoid dirtying .project files unnecessarily. diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java index 99600609b46..3764f3d68f1 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java @@ -326,7 +326,7 @@ public void create(IProjectDescription description, int updateFlags, IProgressMo updateDescription(); // make sure the .location file is written workspace.getMetaArea().writePrivateDescription(this); - } else { + } else if (!desc.isWorkspacePrivate()) { // write out the project writeDescription(IResource.FORCE); } diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescription.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescription.java index 3502ac2f9a7..4f299b1249b 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescription.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescription.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import org.eclipse.core.filesystem.URIUtil; import org.eclipse.core.internal.events.BuildCommand; @@ -124,6 +125,7 @@ public class ProjectDescription extends ModelObject implements IProjectDescripti protected URI location = null; protected volatile String[] natures = EMPTY_STRING_ARRAY; protected URI snapshotLocation = null; + private boolean privateFlag; public ProjectDescription() { super(); @@ -546,6 +548,14 @@ public boolean hasPrivateChanges(ProjectDescription description) { // Configuration level references if (configRefsHaveChanges(dynamicConfigRefs, description.dynamicConfigRefs)) return true; + // has natures changed? + if (!Set.of(natures).equals(Set.of(description.natures))) { + return true; + } + // has buildspec changed? + if (!Objects.deepEquals(buildSpec, description.buildSpec)) { + return true; + } return false; } @@ -978,4 +988,14 @@ private static IProject[] computeDynamicReferencesForProject(IBuildConfiguration } return result.toArray(new IProject[0]); } + + @Override + public boolean isWorkspacePrivate() { + return privateFlag; + } + + @Override + public void setWorkspacePrivate(boolean privateFlag) { + this.privateFlag = privateFlag; + } } diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescriptionReader.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescriptionReader.java index 5b968235e76..e3a05685cc9 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescriptionReader.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectDescriptionReader.java @@ -233,26 +233,31 @@ private void endBuildTriggersElement(String elementName) { state = S_BUILD_COMMAND; BuildCommand command = (BuildCommand) objectStack.peek(); //presence of this element indicates the builder is configurable + String string = charBuffer.toString(); command.setConfigurable(true); //clear all existing values - command.setBuilding(IncrementalProjectBuilder.AUTO_BUILD, false); - command.setBuilding(IncrementalProjectBuilder.CLEAN_BUILD, false); - command.setBuilding(IncrementalProjectBuilder.FULL_BUILD, false); - command.setBuilding(IncrementalProjectBuilder.INCREMENTAL_BUILD, false); - - //set new values according to value in the triggers element - StringTokenizer tokens = new StringTokenizer(charBuffer.toString(), ","); //$NON-NLS-1$ - while (tokens.hasMoreTokens()) { - String next = tokens.nextToken(); - if (next.equalsIgnoreCase(TRIGGER_AUTO)) { - command.setBuilding(IncrementalProjectBuilder.AUTO_BUILD, true); - } else if (next.equalsIgnoreCase(TRIGGER_CLEAN)) { - command.setBuilding(IncrementalProjectBuilder.CLEAN_BUILD, true); - } else if (next.equalsIgnoreCase(TRIGGER_FULL)) { - command.setBuilding(IncrementalProjectBuilder.FULL_BUILD, true); - } else if (next.equalsIgnoreCase(TRIGGER_INCREMENTAL)) { - command.setBuilding(IncrementalProjectBuilder.INCREMENTAL_BUILD, true); - } + parseBuildTriggers(command, string); + } + } + + static void parseBuildTriggers(BuildCommand command, String string) { + command.setBuilding(IncrementalProjectBuilder.AUTO_BUILD, false); + command.setBuilding(IncrementalProjectBuilder.CLEAN_BUILD, false); + command.setBuilding(IncrementalProjectBuilder.FULL_BUILD, false); + command.setBuilding(IncrementalProjectBuilder.INCREMENTAL_BUILD, false); + + // set new values according to value in the triggers element + StringTokenizer tokens = new StringTokenizer(string, ","); //$NON-NLS-1$ + while (tokens.hasMoreTokens()) { + String next = tokens.nextToken(); + if (next.equalsIgnoreCase(TRIGGER_AUTO)) { + command.setBuilding(IncrementalProjectBuilder.AUTO_BUILD, true); + } else if (next.equalsIgnoreCase(TRIGGER_CLEAN)) { + command.setBuilding(IncrementalProjectBuilder.CLEAN_BUILD, true); + } else if (next.equalsIgnoreCase(TRIGGER_FULL)) { + command.setBuilding(IncrementalProjectBuilder.FULL_BUILD, true); + } else if (next.equalsIgnoreCase(TRIGGER_INCREMENTAL)) { + command.setBuilding(IncrementalProjectBuilder.INCREMENTAL_BUILD, true); } } } diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/SaveManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/SaveManager.java index 25493cf919c..bfe3ca99f60 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/SaveManager.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/SaveManager.java @@ -1371,6 +1371,9 @@ protected void saveMetaInfo(MultiStatus problems, IProgressMonitor monitor) thro * @return Status object containing non-critical warnings, or an OK status. */ protected IStatus saveMetaInfo(Project project, IProgressMonitor monitor) throws CoreException { + if (project.internalGetDescription().isWorkspacePrivate()) { + return Status.OK_STATUS; + } long start = System.currentTimeMillis(); //if there is nothing on disk, write the description if (!workspace.getFileSystemManager().hasSavedDescription(project)) { diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProjectDescription.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProjectDescription.java index 7554ff0fa40..d66e645887c 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProjectDescription.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IProjectDescription.java @@ -175,6 +175,13 @@ public interface IProjectDescription { */ ICommand newCommand(); + /** + * @return true if this project is only persisted in the private + * workspace area + * @since 3.23 + */ + boolean isWorkspacePrivate(); + /** * Sets the active configuration for the described project. *

@@ -385,4 +392,12 @@ public interface IProjectDescription { * @see #getReferencedProjects() */ void setReferencedProjects(IProject[] projects); + + /** + * Sets the project to be only persisted into the private workspace area and not + * into a physical .project file in the root of the project folder. + * + * @since 3.23 + */ + void setWorkspacePrivate(boolean privateFlag); }