diff --git a/.gitattributes b/.gitattributes index f88301131..cb3464431 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,6 @@ *.tar binary *.bz2 binary *.gz binary + +# special handling for test files +studio64.exe text diff --git a/.gitignore b/.gitignore index c2db89201..7fe3a6ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.bak .* !.gitignore +!.gitkeep !.ide.software.version !.devon.software.version !.ide @@ -15,6 +16,7 @@ target/ eclipse-target/ generated/ +node_modules # Package Files # *.jar diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java index b4ef21c45..20b53db63 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java @@ -1,7 +1,11 @@ package com.devonfw.tools.ide.io; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; @@ -10,34 +14,35 @@ */ public interface FileAccess { + /** {@link PosixFilePermission}s for "rwxr-xr-x" or 0755. */ + Set RWX_RX_RX = Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE); + /** * Downloads a file from an arbitrary location. * * @param url the location of the binary file to download. May also be a local or remote path to copy from. - * @param targetFile the {@link Path} to the target file to download to. Should not already exists. Missing parent - * directories will be created automatically. + * @param targetFile the {@link Path} to the target file to download to. Should not already exists. Missing parent directories will be created automatically. */ void download(String url, Path targetFile); /** * Creates the entire {@link Path} as directories if not already existing. * - * @param directory the {@link Path} to - * {@link java.nio.file.Files#createDirectories(Path, java.nio.file.attribute.FileAttribute...) create}. + * @param directory the {@link Path} to {@link java.nio.file.Files#createDirectories(Path, java.nio.file.attribute.FileAttribute...) create}. */ void mkdirs(Path directory); /** * @param file the {@link Path} to check. - * @return {@code true} if the given {@code file} points to an existing file, {@code false} otherwise (the given - * {@link Path} does not exist or is a directory). + * @return {@code true} if the given {@code file} points to an existing file, {@code false} otherwise (the given {@link Path} does not exist or is a + * directory). */ boolean isFile(Path file); /** * @param folder the {@link Path} to check. - * @return {@code true} if the given {@code folder} points to an existing directory, {@code false} otherwise (a - * warning is logged in this case). + * @return {@code true} if the given {@code folder} points to an existing directory, {@code false} otherwise (a warning is logged in this case). */ boolean isExpectedFolder(Path folder); @@ -61,9 +66,9 @@ public interface FileAccess { void move(Path source, Path targetDir); /** - * Creates a symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows - * junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, which must - * point to absolute paths. Therefore, the created link will be absolute instead of relative. + * Creates a symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows junction, it will be replaced. In case of + * missing privileges, Windows Junctions may be used as fallback, which must point to absolute paths. Therefore, the created link will be absolute instead of + * relative. * * @param source the source {@link Path} to link to, may be relative or absolute. * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}. @@ -72,9 +77,9 @@ public interface FileAccess { void symlink(Path source, Path targetLink, boolean relative); /** - * Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a - * Windows junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, - * which must point to absolute paths. Therefore, the created link will be absolute instead of relative. + * Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows junction, it will be replaced. In case + * of missing privileges, Windows Junctions may be used as fallback, which must point to absolute paths. Therefore, the created link will be absolute instead + * of relative. * * @param source the source {@link Path} to link to, may be relative or absolute. * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}. @@ -86,8 +91,8 @@ default void symlink(Path source, Path targetLink) { /** * @param source the source {@link Path file or folder} to copy. - * @param target the {@link Path} to copy {@code source} to. See {@link #copy(Path, Path, FileCopyMode)} for details. - * will always ensure that in the end you will find the same content of {@code source} in {@code target}. + * @param target the {@link Path} to copy {@code source} to. See {@link #copy(Path, Path, FileCopyMode)} for details. will always ensure that in the end you + * will find the same content of {@code source} in {@code target}. */ default void copy(Path source, Path target) { @@ -96,14 +101,13 @@ default void copy(Path source, Path target) { /** * @param source the source {@link Path file or folder} to copy. - * @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not - * take the filename of {@code source} and copy that to {@code target} in case that is an existing folder. - * Instead it will always be simple and stupid and just copy from {@code source} to {@code target}. Therefore - * the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. - * While {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method - * will always ensure that in the end you will find the same content of {@code source} in {@code target}. - * @param fileOnly - {@code true} if {@code fileOrFolder} is expected to be a file and an exception shall be thrown if - * it is a directory, {@code false} otherwise (copy recursively). + * @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source} and + * copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to + * {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While + * {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find the + * same content of {@code source} in {@code target}. + * @param fileOnly - {@code true} if {@code fileOrFolder} is expected to be a file and an exception shall be thrown if it is a directory, {@code false} + * otherwise (copy recursively). */ void copy(Path source, Path target, FileCopyMode fileOnly); @@ -119,8 +123,7 @@ default void extract(Path archiveFile, Path targetDir) { /** * @param archiveFile the {@link Path} to the archive file to extract. * @param targetDir the {@link Path} to the directory where to extract the {@code archiveFile}. - * @param postExtractHook the {@link Consumer} to be called after the extraction on the final folder before it is - * moved to {@code targetDir}. + * @param postExtractHook the {@link Consumer} to be called after the extraction on the final folder before it is moved to {@code targetDir}. */ default void extract(Path archiveFile, Path targetDir, Consumer postExtractHook) { @@ -130,15 +133,13 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac /** * @param archiveFile the {@link Path} to the archive file to extract. * @param targetDir the {@link Path} to the directory where to extract the {@code archiveFile}. - * @param postExtractHook the {@link Consumer} to be called after the extraction on the final folder before it is - * moved to {@code targetDir}. + * @param postExtractHook the {@link Consumer} to be called after the extraction on the final folder before it is moved to {@code targetDir}. * @param extract {@code true} if the {@code archiveFile} should be extracted (default), {@code false} otherwise. */ void extract(Path archiveFile, Path targetDir, Consumer postExtractHook, boolean extract); /** - * Extracts a ZIP file what is the common archive format on Windows. Initially invented by PKZIP for MS-DOS and also - * famous from WinZIP software for Windows. + * Extracts a ZIP file what is the common archive format on Windows. Initially invented by PKZIP for MS-DOS and also famous from WinZIP software for Windows. * * @param file the ZIP file to extract. * @param targetDir the {@link Path} with the directory to unzip to. @@ -153,10 +154,9 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac void extractTar(Path file, Path targetDir, TarCompression compression); /** - * Extracts an Apple DMG (Disk Image) file that is similar to an ISO image. DMG files are commonly used for software - * releases on MacOS. Double-clicking such files on MacOS mounts them and show the application together with a - * symbolic link to the central applications folder and some help instructions. The user then copies the application - * to the applications folder via drag and drop in order to perform the installation. + * Extracts an Apple DMG (Disk Image) file that is similar to an ISO image. DMG files are commonly used for software releases on MacOS. Double-clicking such + * files on MacOS mounts them and show the application together with a symbolic link to the central applications folder and some help instructions. The user + * then copies the application to the applications folder via drag and drop in order to perform the installation. * * @param file the DMG file to extract. * @param targetDir the target directory where to extract the contents to. @@ -164,8 +164,8 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac void extractDmg(Path file, Path targetDir); /** - * Extracts an MSI (Microsoft Installer) file. MSI files are commonly used for software releases on Windows that allow - * an installation wizard and easy later uninstallation. + * Extracts an MSI (Microsoft Installer) file. MSI files are commonly used for software releases on Windows that allow an installation wizard and easy later + * uninstallation. * * @param file the MSI file to extract. * @param targetDir the target directory where to extract the contents to. @@ -173,10 +173,9 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac void extractMsi(Path file, Path targetDir); /** - * Extracts an Apple PKG (Package) file. PKG files are used instead of {@link #extractDmg(Path, Path) DMG files} if - * additional changes have to be performed like drivers to be installed. Similar to what - * {@link #extractMsi(Path, Path) MSI} is on Windows. PKG files are internally a xar based archive with a specific - * structure. + * Extracts an Apple PKG (Package) file. PKG files are used instead of {@link #extractDmg(Path, Path) DMG files} if additional changes have to be performed + * like drivers to be installed. Similar to what {@link #extractMsi(Path, Path) MSI} is on Windows. PKG files are internally a xar based archive with a + * specific structure. * * @param file the PKG file to extract. * @param targetDir the target directory where to extract the contents to. @@ -197,11 +196,10 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac void delete(Path path); /** - * Creates a new temporary directory. ATTENTION: The user of this method is responsible to do house-keeping and - * {@link #delete(Path) delete} it after the work is done. + * Creates a new temporary directory. ATTENTION: The user of this method is responsible to do house-keeping and {@link #delete(Path) delete} it after the work + * is done. * - * @param name the default name of the temporary directory to create. A prefix or suffix may be added to ensure - * uniqueness. + * @param name the default name of the temporary directory to create. A prefix or suffix may be added to ensure uniqueness. * @return the {@link Path} to the newly created and unique temporary directory. */ Path createTempDir(String name); @@ -216,10 +214,9 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac /** * @param dir the {@link Path} to the directory where to list the children. - * @param filter the {@link Predicate} used to {@link Predicate#test(Object) decide} which children to include (if - * {@code true} is returned). - * @return all children of the given {@link Path} that match the given {@link Predicate}. Will be the empty list of - * the given {@link Path} is not an existing directory. + * @param filter the {@link Predicate} used to {@link Predicate#test(Object) decide} which children to include (if {@code true} is returned). + * @return all children of the given {@link Path} that match the given {@link Predicate}. Will be the empty list of the given {@link Path} is not an existing + * directory. */ List listChildren(Path dir, Predicate filter); @@ -234,8 +231,20 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac /** * Checks if the given directory is empty. + * * @param dir The {@link Path} object representing the directory to check. * @return {@code true} if the directory is empty, {@code false} otherwise. */ boolean isEmptyDir(Path dir); + + /** + * Makes the file executable. + * + * @param path Path to the file. + * @throws IOException if an I/O error occurs. + */ + default void makeExecutable(Path path) throws IOException { + + Files.setPosixFilePermissions(path, RWX_RX_RX); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java index 1bab5da06..7f2e2ba16 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java @@ -673,7 +673,8 @@ public void extractDmg(Path file, Path targetDir) { if (appPath == null) { throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file); } - copy(appPath, targetDir); + + copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE); pc.addArgs("detach", "-force", mountPath); pc.run(); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java index ae8fd6870..ee1e3f3d8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java @@ -36,7 +36,7 @@ public abstract class LocalToolCommandlet extends ToolCommandlet { * The constructor. * * @param context the {@link IdeContext}. - * @param tool the {@link #getName() tool name}. + * @param tool the {@link #getName() tool name}. * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method. */ public LocalToolCommandlet(IdeContext context, String tool, Set tags) { @@ -138,7 +138,7 @@ public ToolInstallation installInRepo(VersionIdentifier version, String edition) * repository without touching the IDE installation. * * @param version the {@link VersionIdentifier} requested to be installed. May also be a {@link VersionIdentifier#isPattern() version pattern}. - * @param edition the specific edition to install. + * @param edition the specific edition to install. * @param toolRepository the {@link ToolRepository} to use. * @return the {@link ToolInstallation} in the central software repository matching the given {@code version}. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java b/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java index 8fe98acbe..1cf6d9963 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java @@ -1,11 +1,18 @@ package com.devonfw.tools.ide.tool.intellij; +import com.devonfw.tools.ide.cli.CliArgument; import com.devonfw.tools.ide.common.Tag; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.io.FileAccess; +import com.devonfw.tools.ide.process.ProcessMode; import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet; import com.devonfw.tools.ide.tool.ide.PluginDescriptor; import com.devonfw.tools.ide.tool.java.Java; +import com.devonfw.tools.ide.version.VersionIdentifier; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Set; /** @@ -13,6 +20,12 @@ */ public class Intellij extends IdeToolCommandlet { + private static final String IDEA = "idea"; + + private static final String IDEA64_EXE = IDEA + "64.exe"; + + private static final String IDEA_BASH_SCRIPT = IDEA + ".sh"; + /** * The constructor. * @@ -23,6 +36,27 @@ public Intellij(IdeContext context) { super(context, "intellij", Set.of(Tag.INTELLIJ)); } + @Override + public void runTool(ProcessMode processMode, VersionIdentifier toolVersion, String... args) { + + install(true); + args = CliArgument.prepend(args, this.context.getWorkspacePath().toString()); + super.runTool(processMode, toolVersion, args); + } + + @Override + protected String getBinaryName() { + + Path toolBinPath = getToolBinPath(); + if (this.context.getSystemInfo().isWindows()) { + return toolBinPath.resolve(IDEA64_EXE).toString(); + } else if (this.context.getSystemInfo().isLinux()) { + return toolBinPath.resolve(IDEA_BASH_SCRIPT).toString(); + } else { + return getToolPath().resolve("IntelliJ IDEA" + generateMacEditionString() + ".app").resolve("Contents").resolve("MacOS").resolve(IDEA).toString(); + } + } + @Override public boolean install(boolean silent) { @@ -31,10 +65,37 @@ public boolean install(boolean silent) { } @Override - public void installPlugin(PluginDescriptor plugin) { + protected void postInstall() { + + super.postInstall(); + if (this.context.getSystemInfo().isMac()) { + setMacOsFilePermissions(getToolPath().resolve("IntelliJ IDEA" + generateMacEditionString() + ".app").resolve("Contents").resolve("MacOS").resolve(IDEA)); + } + } + + private String generateMacEditionString() { - // TODO Auto-generated method stub + String edition = ""; + if (getEdition().equals("intellij")) { + edition = " CE"; + } + return edition; + } + + private void setMacOsFilePermissions(Path binaryFile) { + if (Files.exists(binaryFile)) { + FileAccess fileAccess = this.context.getFileAccess(); + try { + fileAccess.makeExecutable(binaryFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } -} \ No newline at end of file + @Override + public void installPlugin(PluginDescriptor plugin) { + // TODO: needs to be implemented see: https://github.com/devonfw/IDEasy/issues/433 + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java new file mode 100644 index 000000000..12e5155f6 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java @@ -0,0 +1,83 @@ +package com.devonfw.tools.ide.tool.intellij; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.os.SystemInfo; +import com.devonfw.tools.ide.os.SystemInfoMock; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.nio.file.Path; + +/** + * Integration test of {@link Intellij}. + */ +public class IntellijTest extends AbstractIdeContextTest { + + private static final String PROJECT_INTELLIJ = "intellij"; + + private final IdeTestContext context = newContext(PROJECT_INTELLIJ); + + /** + * Tests if the {@link Intellij} can be installed properly. + * + * @param os String of the OS to use. + */ + @ParameterizedTest + @ValueSource(strings = { "windows", "mac", "linux" }) + public void testIntellijInstall(String os) { + + // arrange + SystemInfo systemInfo = SystemInfoMock.of(os); + context.setSystemInfo(systemInfo); + Intellij commandlet = new Intellij(context); + + // act + commandlet.install(); + + // assert + checkInstallation(context); + + //if tool already installed + commandlet.install(); + assertLogMessage(context, IdeLogLevel.DEBUG, "Version 2023.3.3 of tool intellij is already installed"); + } + + /** + * Tests if {@link Intellij IntelliJ IDE} can be run. + * + * @param os String of the OS to use. + */ + @ParameterizedTest + @ValueSource(strings = { "windows", "mac", "linux" }) + public void testIntellijRun(String os) { + // arrange + SystemInfo systemInfo = SystemInfoMock.of(os); + context.setSystemInfo(systemInfo); + Intellij commandlet = new Intellij(context); + + // act + commandlet.run(); + + // assert + SystemInfo currentSystemInfo = context.getSystemInfo(); + Path workspacePath = this.context.getWorkspacePath(); + + if (currentSystemInfo.isMac()) { + assertLogMessage(context, IdeLogLevel.INFO, "intellij mac " + workspacePath); + } else if (currentSystemInfo.isLinux()) { + assertLogMessage(context, IdeLogLevel.INFO, "intellij linux " + workspacePath); + } else if (currentSystemInfo.isWindows()) { + assertLogMessage(context, IdeLogLevel.INFO, "intellij windows " + workspacePath); + } + checkInstallation(context); + } + + private void checkInstallation(IdeTestContext context) { + + assertThat(context.getSoftwarePath().resolve("intellij/.ide.software.version")).exists().hasContent("2023.3.3"); + assertLogMessage(context, IdeLogLevel.SUCCESS, "Successfully installed java in version 17.0.10_7"); + assertLogMessage(context, IdeLogLevel.SUCCESS, "Successfully installed intellij in version 2023.3.3"); + } +} diff --git a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/linux/InstallTest.txt b/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/linux/InstallTest.txt deleted file mode 100644 index 6de7b8c69..000000000 --- a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/linux/InstallTest.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test file. diff --git a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/mac/InstallTest.txt b/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/mac/InstallTest.txt deleted file mode 100644 index 6de7b8c69..000000000 --- a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/mac/InstallTest.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test file. diff --git a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/mac/bin/idea.sh b/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/mac/IntelliJ IDEA CE.app/Contents/MacOS/idea similarity index 100% rename from cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/mac/bin/idea.sh rename to cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/mac/IntelliJ IDEA CE.app/Contents/MacOS/idea diff --git a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/windows/InstallTest.txt b/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/windows/InstallTest.txt deleted file mode 100644 index 6de7b8c69..000000000 --- a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/windows/InstallTest.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test file. diff --git a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/windows/bin/idea64.exe b/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/windows/bin/idea64.exe index e275e0cc4..549da5d40 100644 Binary files a/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/windows/bin/idea64.exe and b/cli/src/test/resources/ide-projects/intellij/repository/intellij/intellij/default/windows/bin/idea64.exe differ diff --git a/cli/src/test/resources/ide-projects/intellij/repository/java/java/default/bin/readme b/cli/src/test/resources/ide-projects/intellij/repository/java/java/default/bin/readme new file mode 100644 index 000000000..e69de29bb