Skip to content

Commit

Permalink
Merge branch 'feature/208-MockOutToolRepoRefactorTestInfra' into feat…
Browse files Browse the repository at this point in the history
…ure/31-Implement-ToolCommandlet-for-Node-Package-Manager
  • Loading branch information
ndemirca committed Feb 27, 2024
2 parents 5959f79 + c0399aa commit a59bb6a
Show file tree
Hide file tree
Showing 68 changed files with 858 additions and 224 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.devonfw.tools.ide.commandlet.FileExtractor;

import java.nio.file.Path;

import com.devonfw.tools.ide.tool.ToolCommandlet;

/**
* {@link CommandletFileExtractor} class which handles the extraction of downloaded installations of a
* {@link ToolCommandlet}.
*/
public interface CommandletFileExtractor {

/**
*
* @param file file the {@link Path} to the file to extract.
* @param targetDir targetDir the {@link Path} to the directory where to extract (or copy) the file.
* @param isExtract {@code true} if the tool is truly extracted, {@code false} the tool will be moved to the targetDir
* (e.g. when an installer exists).
*/
void extract(Path file, Path targetDir, boolean isExtract);

/**
* Moves the extracted content to the final destination {@link Path}. May be overridden to customize the extraction
* process.
*
* @param from the source {@link Path} to move.
* @param to the target {@link Path} to move to.
*/
void moveAndProcessExtraction(Path from, Path to);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.devonfw.tools.ide.commandlet.FileExtractor;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.io.TarCompression;
import com.devonfw.tools.ide.process.ProcessContext;
import com.devonfw.tools.ide.tool.ToolCommandlet;
import com.devonfw.tools.ide.util.FilenameUtil;

/**
* Implements common {@link CommandletFileExtractor} for a {@link ToolCommandlet}
*/
public class CommandletFileExtractorImpl implements CommandletFileExtractor {

protected final IdeContext context;

protected final ToolCommandlet commandlet;

/**
* The constructor
*
* @param context the {@link IdeContext}.
* @param commandlet the {@link ToolCommandlet}
*/
public CommandletFileExtractorImpl(IdeContext context, ToolCommandlet commandlet) {

this.context = context;
this.commandlet = commandlet;
}

@Override
public void extract(Path file, Path targetDir, boolean isExtract) {

FileAccess fileAccess = this.context.getFileAccess();
if (isExtract) {
Path tmpDir = fileAccess.createTempDir("extract-" + file.getFileName());
this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", file, tmpDir, targetDir);
String extension = FilenameUtil.getExtension(file.getFileName().toString());
this.context.trace("Determined file extension {}", extension);
TarCompression tarCompression = TarCompression.of(extension);
ExtractorFileType fileType = ExtractorFileType.valueOf(extension.toUpperCase());

if (tarCompression != null) {
fileAccess.untar(file, tmpDir, tarCompression);
} else if (fileType == ExtractorFileType.ZIP || fileType == ExtractorFileType.JAR) {
fileAccess.unzip(file, tmpDir);
} else if (fileType == ExtractorFileType.DMG) {
extractLinuxDmg(file, tmpDir);
} else if (fileType == ExtractorFileType.MSI) {
extractWindowsMsi(file, tmpDir);
} else if (fileType == ExtractorFileType.PKG) {
extractPkg(file, tmpDir);

} else {
throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + file);
}
moveAndProcessExtraction(getProperInstallationSubDirOf(tmpDir), targetDir);
fileAccess.delete(tmpDir);
} else {
this.context.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.",
commandlet.getName(), file);

if (Files.isDirectory(file)) {
fileAccess.move(file, targetDir);
} else {
try {
Files.createDirectories(targetDir);
} catch (IOException e) {
throw new IllegalStateException("Failed to create folder " + targetDir);
}
fileAccess.move(file, targetDir.resolve(file.getFileName()));
}
}
}

@Override
public void moveAndProcessExtraction(Path from, Path to) {

this.context.getFileAccess().move(from, to);
}

/**
* @param path the {@link Path} to start the recursive search from.
* @return the deepest subdir {@code s} of the passed path such that all directories between {@code s} and the passed
* path (including {@code s}) are the sole item in their respective directory and {@code s} is not named
* "bin".
*/
private Path getProperInstallationSubDirOf(Path path) {

try (Stream<Path> stream = Files.list(path)) {
Path[] subFiles = stream.toArray(Path[]::new);
if (subFiles.length == 0) {
throw new CliException("The downloaded package for the tool " + commandlet.getName()
+ " seems to be empty as you can check in the extracted folder " + path);
} else if (subFiles.length == 1) {
String filename = subFiles[0].getFileName().toString();
if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS)
&& !filename.endsWith(".app") && Files.isDirectory(subFiles[0])) {
return getProperInstallationSubDirOf(subFiles[0]);
}
}
return path;
} catch (IOException e) {
throw new IllegalStateException("Failed to get sub-files of " + path);
}
}

private void extractLinuxDmg(Path file, Path tmpDir) {

assert this.context.getSystemInfo().isMac();

FileAccess fileAccess = this.context.getFileAccess();
Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
fileAccess.mkdirs(mountPath);
ProcessContext pc = this.context.newProcess();
pc.executable("hdiutil");
pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
pc.run();
Path appPath = fileAccess.findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
if (appPath == null) {
throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
}
fileAccess.copy(appPath, tmpDir);
pc.addArgs("detach", "-force", mountPath);
pc.run();
}

private void extractWindowsMsi(Path file, Path tmpDir) {

FileAccess fileAccess = this.context.getFileAccess();
this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + tmpDir).run();
// msiexec also creates a copy of the MSI
Path msiCopy = tmpDir.resolve(file.getFileName());
fileAccess.delete(msiCopy);
}

private void extractPkg(Path file, Path tmpDir) {

FileAccess fileAccess = this.context.getFileAccess();

Path tmpDirPkg = fileAccess.createTempDir("ide-pkg-");
ProcessContext pc = this.context.newProcess();
// we might also be able to use cpio from commons-compression instead of external xar...
pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
Path contentPath = fileAccess.findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
fileAccess.untar(contentPath, tmpDir, TarCompression.GZ);
fileAccess.delete(tmpDirPkg);

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.devonfw.tools.ide.commandlet.FileExtractor;

enum ExtractorFileType {
ZIP, JAR, DMG, MSI, PKG
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,19 @@ public abstract class AbstractIdeContext implements IdeContext {

private UrlMetadata urlMetadata;

private Path defaultExecutionDirectory;

/**
* The constructor.
*
* @param minLogLevel the minimum {@link IdeLogLevel} to enable. Should be {@link IdeLogLevel#INFO} by default.
* @param factory the {@link Function} to create {@link IdeSubLogger} per {@link IdeLogLevel}.
* @param userDir the optional {@link Path} to current working directory.
* @param toolRepository @param toolRepository the {@link ToolRepository} of the context. If it is set to {@code null}
* {@link DefaultToolRepository} will be used.
*/
public AbstractIdeContext(IdeLogLevel minLogLevel, Function<IdeLogLevel, IdeSubLogger> factory, Path userDir) {
public AbstractIdeContext(IdeLogLevel minLogLevel, Function<IdeLogLevel, IdeSubLogger> factory, Path userDir,
ToolRepository toolRepository) {

super();
this.loggerFactory = factory;
Expand Down Expand Up @@ -232,7 +237,13 @@ public AbstractIdeContext(IdeLogLevel minLogLevel, Function<IdeLogLevel, IdeSubL
this.downloadPath = this.userHome.resolve("Downloads/ide");
this.variables = createVariables();
this.path = computeSystemPath();
this.defaultToolRepository = new DefaultToolRepository(this);

if (toolRepository == null) {
this.defaultToolRepository = new DefaultToolRepository(this);
} else {
this.defaultToolRepository = toolRepository;
}

this.customToolRepository = CustomToolRepositoryImpl.of(this);
this.workspaceMerger = new DirectoryMerger(this);
}
Expand Down Expand Up @@ -590,6 +601,25 @@ public DirectoryMerger getWorkspaceMerger() {
return this.workspaceMerger;
}

/**
*
* @return the {@link #defaultExecutionDirectory} the directory in which a command process is executed.
*/
public Path getDefaultExecutionDirectory() {

return defaultExecutionDirectory;
}

/**
* @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
*/
public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {

if (defaultExecutionDirectory != null) {
this.defaultExecutionDirectory = defaultExecutionDirectory;
}
}

@Override
public GitContext getGitContext() {

Expand All @@ -599,7 +629,9 @@ public GitContext getGitContext() {
@Override
public ProcessContext newProcess() {

return new ProcessContextImpl(this);
ProcessContext processContext = new ProcessContextImpl(this);
processContext.directory(this.getDefaultExecutionDirectory());
return processContext;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class IdeContextConsole extends AbstractIdeContext {
*/
public IdeContextConsole(IdeLogLevel minLogLevel, Appendable out, boolean colored) {

super(minLogLevel, level -> new IdeSubLoggerOut(level, out, colored), null);
super(minLogLevel, level -> new IdeSubLoggerOut(level, out, colored), null, null);
if (System.console() == null) {
debug("System console not available - using System.in as fallback");
this.scanner = new Scanner(System.in);
Expand All @@ -51,12 +51,12 @@ public IdeProgressBar prepareProgressBar(String taskName, long size) {
ProgressBarBuilder pbb = new ProgressBarBuilder();
// default (COLORFUL_UNICODE_BLOCK)
pbb.setStyle(ProgressBarStyle.builder().refreshPrompt("\r").leftBracket("\u001b[33m│").delimitingSequence("")
.rightBracket("│\u001b[0m").block('█').space(' ').fractionSymbols(" ▏▎▍▌▋▊▉").rightSideFractionSymbol(' ')
.build());
.rightBracket("│\u001b[0m").block('█').space(' ').fractionSymbols(" ▏▎▍▌▋▊▉").rightSideFractionSymbol(' ')
.build());
// set different style for Windows systems (ASCII)
if (this.getSystemInfo().isWindows()) {
pbb.setStyle(ProgressBarStyle.builder().refreshPrompt("\r").leftBracket("[").delimitingSequence("")
.rightBracket("]").block('=').space(' ').fractionSymbols(">").rightSideFractionSymbol(' ').build());
.rightBracket("]").block('=').space(' ').fractionSymbols(">").rightSideFractionSymbol(' ').build());
}
pbb.showSpeed();
pbb.setTaskName(taskName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ public ProcessContext errorHandling(ProcessErrorHandling handling) {
@Override
public ProcessContext directory(Path directory) {

this.processBuilder.directory(directory.toFile());
if (directory != null) {
this.processBuilder.directory(directory.toFile());
} else {
context.debug(
"Could not set the process builder's working directory! Directory of the current java process is used.");
}

return this;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.devonfw.tools.ide.tool;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;

import com.devonfw.tools.ide.common.Tag;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.io.FileAccess;
Expand All @@ -9,10 +13,6 @@
import com.devonfw.tools.ide.repo.ToolRepository;
import com.devonfw.tools.ide.version.VersionIdentifier;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;

/**
* {@link ToolCommandlet} that is installed globally.
*/
Expand Down Expand Up @@ -58,7 +58,7 @@ protected boolean doInstall(boolean silent) {
Path target = toolRepository.download(this.tool, edition, resolvedVersion);
Path tmpDir = fileAccess.createTempDir(getName());
Path downloadBinaryPath = tmpDir.resolve(target.getFileName());
extract(target, downloadBinaryPath);
commandletFileExtractor.extract(target, downloadBinaryPath, isExtract());
if (isExtract()) {
downloadBinaryPath = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.nio.file.StandardOpenOption;
import java.util.Set;

import com.devonfw.tools.ide.commandlet.Commandlet;
import com.devonfw.tools.ide.common.Tag;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.io.FileAccess;
Expand Down Expand Up @@ -92,8 +91,9 @@ protected boolean doInstall(boolean silent) {
}

/**
* Performs the installation of the {@link #getName() tool} managed by this {@link Commandlet} only in the central
* software repository without touching the IDE installation.
* Performs the installation of the {@link #getName() tool} managed by this
* {@link com.devonfw.tools.ide.commandlet.Commandlet} only in the central software repository without touching the
* IDE installation.
*
* @param version the {@link VersionIdentifier} requested to be installed. May also be a
* {@link VersionIdentifier#isPattern() version pattern}.
Expand All @@ -105,8 +105,9 @@ public ToolInstallation installInRepo(VersionIdentifier version) {
}

/**
* Performs the installation of the {@link #getName() tool} managed by this {@link Commandlet} only in the central
* software repository without touching the IDE installation.
* Performs the installation of the {@link #getName() tool} managed by this
* {@link com.devonfw.tools.ide.commandlet.Commandlet} only in the central software repository without touching the
* IDE installation.
*
* @param version the {@link VersionIdentifier} requested to be installed. May also be a
* {@link VersionIdentifier#isPattern() version pattern}.
Expand All @@ -119,8 +120,9 @@ public ToolInstallation installInRepo(VersionIdentifier version, String edition)
}

/**
* Performs the installation of the {@link #getName() tool} managed by this {@link Commandlet} only in the central
* software repository without touching the IDE installation.
* Performs the installation of the {@link #getName() tool} managed by this
* {@link com.devonfw.tools.ide.commandlet.Commandlet} only in the central software repository without touching the
* IDE installation.
*
* @param version the {@link VersionIdentifier} requested to be installed. May also be a
* {@link VersionIdentifier#isPattern() version pattern}.
Expand Down Expand Up @@ -151,7 +153,7 @@ public ToolInstallation installInRepo(VersionIdentifier version, String edition,
}
Path target = toolRepository.download(this.tool, edition, resolvedVersion);
fileAccess.mkdirs(toolPath.getParent());
extract(target, toolPath);
commandletFileExtractor.extract(target, toolPath, isExtract());
try {
Files.writeString(toolVersionFile, resolvedVersion.toString(), StandardOpenOption.CREATE_NEW);
} catch (IOException e) {
Expand Down
Loading

0 comments on commit a59bb6a

Please sign in to comment.