Skip to content
13 changes: 11 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/tool/NativePackage.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class NativePackage {
private final List<String> extraInstallOptions;
private final List<String> setupCommands;
private final List<String> cleanupCommands;
private final List<String> optionalNativePackageArtifactPaths;

/**
* Creates a new {@link NativePackage} with optional fields defaulting to empty lists.
Expand All @@ -24,12 +25,13 @@ public class NativePackage {
* @param cleanupCommands commands to run after uninstall (optional)
*/
public NativePackage(NativePackageManager pm, List<String> packages,
List<String> extraInstallOptions, List<String> setupCommands, List<String> cleanupCommands) {
List<String> extraInstallOptions, List<String> setupCommands, List<String> cleanupCommands, List<String> optionalNativePackageArtifactPaths) {
this.packageManager = Objects.requireNonNull(pm, "package manager must not be null");
this.packages = List.copyOf(Objects.requireNonNull(packages, "packages must not be null"));
this.extraInstallOptions = extraInstallOptions != null ? List.copyOf(extraInstallOptions) : List.of();
this.setupCommands = setupCommands != null ? List.copyOf(setupCommands) : List.of();
this.cleanupCommands = cleanupCommands != null ? List.copyOf(cleanupCommands) : List.of();
this.optionalNativePackageArtifactPaths = optionalNativePackageArtifactPaths != null ? List.copyOf(optionalNativePackageArtifactPaths) : List.of();
}

/**
Expand All @@ -39,7 +41,7 @@ public NativePackage(NativePackageManager pm, List<String> packages,
* @param packages the packages that need to be handled
*/
public NativePackage(NativePackageManager pm, List<String> packages) {
this(pm, packages, null, null, null);
this(pm, packages, null, null, null, null);
}

/**
Expand Down Expand Up @@ -88,6 +90,13 @@ public List<String> getCleanupCommands() {
return cleanupCommands;
}

/**
* @return set {@link optionalNativePackageArtifactPaths}.
*/
public List<String> getOptionalNativePackageArtifactPath() {
return optionalNativePackageArtifactPaths;
}

/**
* @param version the version to pin the {@link #getPackages()} to or {@code null} to install the latest available version.
* @return {@link PackageManagerCommand} for installation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,14 @@ public PackageManagerCommand install(NativePackage nativePackage, String version
command.append(' ').append(option);
}
command.append(' ').append(this.installCommand);
for (String pkg : nativePackage.getPackages()) {
command.append(' ').append(getPackageSpec(pkg, version));
if (nativePackage.getOptionalNativePackageArtifactPath().isEmpty()) {
for (String pkg : nativePackage.getPackages()) {
command.append(' ').append(getPackageSpec(pkg, version));
}
} else {
for (String nativePackageArtifactPath : nativePackage.getOptionalNativePackageArtifactPath()) {
command.append(' ').append(nativePackageArtifactPath);
}
}
commands.add(command.toString());
return new PackageManagerCommand(this, commands);
Expand Down
63 changes: 62 additions & 1 deletion cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.devonfw.tools.ide.tool.docker;

import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
Expand All @@ -13,6 +14,10 @@
import com.devonfw.tools.ide.tool.GlobalToolCommandlet;
import com.devonfw.tools.ide.tool.NativePackage;
import com.devonfw.tools.ide.tool.NativePackageManager;
import com.devonfw.tools.ide.tool.PackageManagerCommand;
import com.devonfw.tools.ide.tool.ToolInstallRequest;
import com.devonfw.tools.ide.tool.ToolInstallation;
import com.devonfw.tools.ide.tool.repository.ToolRepository;
import com.devonfw.tools.ide.version.VersionIdentifier;

/**
Expand All @@ -30,6 +35,10 @@ public class Docker extends GlobalToolCommandlet {

private static final Pattern DOCKER_DESKTOP_LINUX_VERSION_PATTERN = Pattern.compile("^([0-9]+(?:\\.[0-9]+){1,2})");

private static final String EDITION_DOCKER = "docker";

private Path downloadedDebPackageForDocker;

/**
* The constructor.
*
Expand Down Expand Up @@ -65,12 +74,41 @@ private String detectContainerRuntime() {

@Override
protected List<NativePackage> getNativePackages() {

if (EDITION_DOCKER.equals(getConfiguredEdition())) {

List<String> artifactPaths = (this.downloadedDebPackageForDocker == null) ? List.of() : List.of(this.downloadedDebPackageForDocker.toString());

return List.of(
new NativePackage(
NativePackageManager.APT,
List.of("docker-desktop"),
List.of("--allow-downgrades"),
List.of(
"sudo install -m 0755 -d /etc/apt/keyrings",
"sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc",
"sudo chmod a+r /etc/apt/keyrings/docker.asc",
"echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] "
+ "https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo \\\"$VERSION_CODENAME\\\") stable\" | "
+ "sudo tee /etc/apt/sources.list.d/docker.list > /dev/null",
"sudo apt update"
),
List.of(
"sudo rm -f /etc/apt/sources.list.d/docker.list",
"sudo rm -f /etc/apt/keyrings/docker.asc"
),
artifactPaths
)
);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing Rancher Desktop installation remains unchanged.

The regular NativePackage definition for the Docker edition is still required. This definition uses the installed package name docker-desktop, rather than the temporary download path. The package name is needed for package-related operations after installation, especially uninstallation. This allows to create the correct uninstall command:
sudo apt -y autoremove --purge docker-desktop

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO the best way to handle this issue is to extend NativePackage so that it can also support package artifacts (e.g. downloaded .deb or .rpm files). e.g.

private final List<String> optionalNativePackageArtifactPaths;

This would require updating the existing constructor and fixing the resulting compilation issues.

If the list is empty, installation continues to use the package names returned by getPackages(), which is the current behavior.

If artifact paths are provided, NativePackageManager.install(...) should use those paths instead. This would allow us to support tools such as Docker Desktop that require installation from a downloaded package artifact while keeping the existing package-based installation mechanism unchanged.

something like this in NativePackageManager.java

/**
   * @param version the version of the package to install.
   * @param nativePackage the {@link NativePackage} to install.
   * @return the {@link PackageManagerCommand} to install the given {@link NativePackage} including its {@link NativePackage#getSetupCommands()} setup commands.
   */
  public PackageManagerCommand install(NativePackage nativePackage, String version) {
    verifyPackageManager(nativePackage);
    List<String> commands = new ArrayList<>(nativePackage.getSetupCommands());
    StringBuilder command = new StringBuilder(SUDO).append(' ').append(getBinaryName());
    for (String option : nativePackage.getExtraInstallOptions()) {
      command.append(' ').append(option);
    }
    command.append(' ').append(this.installCommand);
    if (nativePackage.getOptionalNativePackageArtifactPath().isEmpty()) {
      for (String pkg : nativePackage.getPackages()) {
        command.append(' ').append(getPackageSpec(pkg, version));
      }
    } else {
      for (String nativePackageArtifactPath : nativePackage.getOptionalNativePackageArtifactPath()) {
        command.append(' ').append(nativePackageArtifactPath);
      }
    }
    commands.add(command.toString());
    return new PackageManagerCommand(this, commands);
  }

It would also be nice if you could extend the existing tests to cover this new functionality.

Comment on lines 75 to +103

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO we should not download the package as part of getPackageManagerCommands(). It feels a bit unexpected to perform a side effect while merely resolving the package manager commands.

Instead, we could download the .deb earlier as part of the installation flow (for example by overriding doInstall()), store the downloaded path, and then pass that path through getNativePackages() just like we do for other native packages.

Something along these lines:

Suggested change
@Override
protected List<NativePackage> getNativePackages() {
if (EDITION_DOCKER.equals(getConfiguredEdition())) {
return List.of(
new NativePackage(
NativePackageManager.APT,
List.of("docker-desktop"),
List.of("--allow-downgrades"),
List.of(),
List.of(
"sudo rm -f /etc/apt/sources.list.d/docker.list",
"sudo rm -f /etc/apt/keyrings/docker.asc"
)
)
);
}
private Path downloadedDebPackageForDocker;
@Override
protected ToolInstallation doInstall(ToolInstallRequest request) {
if (EDITION_DOCKER.equals(getConfiguredEdition())) {
downloadDebPackageStepAndSetPackagePath(request.getRequested().getResolvedVersion());
}
return super.doInstall(request);
}
private void downloadDebPackageStepAndSetPackagePath(VersionIdentifier resolvedVersion) {
ToolRepository toolRepository = this.context.getDefaultToolRepository();
this.downloadedDebPackageForDocker = toolRepository.download(this.tool, EDITION_DOCKER, resolvedVersion, this);
}
@Override
protected List<NativePackage> getNativePackages() {
if (EDITION_DOCKER.equals(getConfiguredEdition())) {
return List.of(
new NativePackage(
NativePackageManager.APT,
List.of(downloadedDebPackageForDocker.toString()),
List.of("--allow-downgrades"),
List.of(
"sudo install -m 0755 -d /etc/apt/keyrings",
"sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc",
"sudo chmod a+r /etc/apt/keyrings/docker.asc",
"echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] "
+ "https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo \\\"$VERSION_CODENAME\\\") stable\" | "
+ "sudo tee /etc/apt/sources.list.d/docker.list > /dev/null",
"sudo apt update"
),
List.of(
"sudo rm -f /etc/apt/sources.list.d/docker.list",
"sudo rm -f /etc/apt/keyrings/docker.asc"
)
);
}


return List.of(
new NativePackage(
NativePackageManager.ZYPPER,
List.of("rancher-desktop"),
List.of("--no-gpg-checks"),
List.of("sudo zypper addrepo https://download.opensuse.org/repositories/isv:/Rancher:/stable/rpm/isv:Rancher:stable.repo"),
null,
null
),
new NativePackage(
Expand All @@ -88,11 +126,34 @@ protected List<NativePackage> getNativePackages() {
List.of(
"sudo rm -f /etc/apt/sources.list.d/isv-rancher-stable.list",
"sudo rm -f /usr/share/keyrings/isv-rancher-stable-archive-keyring.gpg"
)
),
null
)
);
}

@Override
protected ToolInstallation doInstall(ToolInstallRequest request) {
if (EDITION_DOCKER.equals(getConfiguredEdition())) {
downloadDebPackageStepAndSetPackagePath(request.getRequested().getResolvedVersion());
}
return super.doInstall(request);
}

private void downloadDebPackageStepAndSetPackagePath(VersionIdentifier resolvedVersion) {
ToolRepository toolRepository = this.context.getDefaultToolRepository();
this.downloadedDebPackageForDocker = toolRepository.download(this.tool, EDITION_DOCKER, resolvedVersion, this);
}

@Override
protected List<PackageManagerCommand> getInstallPackageManagerCommands(VersionIdentifier resolvedVersion) {
if (!EDITION_DOCKER.equals(getConfiguredEdition())) {
return super.getInstallPackageManagerCommands(resolvedVersion);
}

return getNativePackages().stream().map(nativePackage -> nativePackage.install(null)).toList();
}

@Override
public boolean isExtract() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ protected List<NativePackage> getNativePackages() {
"sudo sh -c 'echo \"deb [signed-by=/usr/share/keyrings/packages-pgadmin-org.gpg] "
+ "https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main\" "
+ "> /etc/apt/sources.list.d/pgadmin4.list && apt update'"),
List.of("sudo rm -f /etc/apt/sources.list.d/pgadmin4.list", "sudo rm -f /usr/share/keyrings/packages-pgadmin-org.gpg")
List.of("sudo rm -f /etc/apt/sources.list.d/pgadmin4.list", "sudo rm -f /usr/share/keyrings/packages-pgadmin-org.gpg"),
null
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ protected List<NativePackage> getNativePackages() {
List.of("mytool"),
List.of(),
List.of(),
List.of("sudo rm -f /etc/apt/sources.list.d/mytool.list"))
List.of("sudo rm -f /etc/apt/sources.list.d/mytool.list"),
List.of())
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ void testAptInstallCommand() {
"sudo apt update"),
List.of(
"sudo rm -f /etc/apt/sources.list.d/example.list",
"sudo rm -f /usr/share/keyrings/example.gpg"));
"sudo rm -f /usr/share/keyrings/example.gpg"),
List.of());

var cmd = NativePackageManager.APT.install(np, "1.0.0");

Expand All @@ -47,7 +48,8 @@ void testAptUninstallCommand() {
"sudo apt update"),
List.of(
"sudo rm -f /etc/apt/sources.list.d/example.list",
"sudo rm -f /usr/share/keyrings/example.gpg"));
"sudo rm -f /usr/share/keyrings/example.gpg"),
List.of());

var cmd = NativePackageManager.APT.uninstall(np);

Expand All @@ -68,7 +70,8 @@ void testZypperInstallCommand() {
"sudo zypper addrepo https://example.com/repo.repo",
"sudo zypper refresh"),
List.of(
"sudo zypper removerepo example-repo"));
"sudo zypper removerepo example-repo"),
List.of());

var cmd = NativePackageManager.ZYPPER.install(np, "1.0.0");

Expand All @@ -89,7 +92,8 @@ void testZypperUninstallCommand() {
"sudo zypper addrepo https://example.com/repo.repo",
"sudo zypper refresh"),
List.of(
"sudo zypper removerepo example-repo"));
"sudo zypper removerepo example-repo"),
List.of());

var cmd = NativePackageManager.ZYPPER.uninstall(np);

Expand All @@ -109,7 +113,8 @@ void testYumInstallCommand() {
"sudo yum-config-manager --add-repo https://example.com/repo.repo",
"sudo yum makecache"),
List.of(
"sudo rm -f /etc/yum.repos.d/example.repo"));
"sudo rm -f /etc/yum.repos.d/example.repo"),
List.of());

var cmd = NativePackageManager.YUM.install(np, "1.0.0");

Expand All @@ -127,7 +132,8 @@ void testYumUninstallCommand() {
List.of("pkg1"),
List.of("--skip-broken"),
List.of("sudo yum-config-manager --add-repo https://example.com/repo.repo", "sudo yum makecache"),
List.of("sudo rm -f /etc/yum.repos.d/example.repo"));
List.of("sudo rm -f /etc/yum.repos.d/example.repo"),
List.of());

var cmd = NativePackageManager.YUM.uninstall(np);

Expand All @@ -143,7 +149,8 @@ void testDnfInstallCommand() {
List.of("pkg1"),
List.of("--refresh"),
List.of("sudo dnf config-manager addrepo --from-repofile=https://example.com/repo.repo", "sudo dnf makecache"),
List.of("sudo rm -f /etc/yum.repos.d/example.repo"));
List.of("sudo rm -f /etc/yum.repos.d/example.repo"),
List.of());

var cmd = NativePackageManager.DNF.install(np, "1.0.0");

Expand All @@ -159,7 +166,8 @@ void testDnfUninstallCommand() {
List.of("pkg1"),
List.of("--refresh"),
List.of("sudo dnf config-manager addrepo --from-repofile=https://example.com/repo.repo", "sudo dnf makecache"),
List.of("sudo rm -f /etc/yum.repos.d/example.repo"));
List.of("sudo rm -f /etc/yum.repos.d/example.repo"),
List.of());

var cmd = NativePackageManager.DNF.uninstall(np);

Expand Down Expand Up @@ -192,4 +200,51 @@ void testParseVersionQueryOutputForRpmBasedPackageManager() {
assertThat(NativePackageManager.YUM.parseVersionQueryOutput("1.0.0")).isEqualTo("1.0.0");
assertThat(NativePackageManager.DNF.parseVersionQueryOutput("1.0.0")).isEqualTo("1.0.0");
}

@Test
void installUsesPackageNameAndVersionWhenNoArtifactPathConfigured() {
NativePackage nativePackage = new NativePackage(NativePackageManager.APT, List.of("docker-desktop"));
PackageManagerCommand result = NativePackageManager.APT.install(nativePackage, "1.2.3");

List<String> commands = result.commands();
String installCommand = commands.getLast();
assertThat(installCommand).contains("apt", "install -y", "docker-desktop=1.2.3*");
assertThat(installCommand).doesNotContain(".deb");
}

@Test
void installUsesArtifactPathWhenConfiguredInsteadOfPackageName() {
String debPath = "/tmp/downloads/docker-desktop-4.34.0-amd64";

NativePackage nativePackage = new NativePackage(NativePackageManager.APT, List.of("docker-desktop"), null, null, null, List.of(debPath));

PackageManagerCommand result = NativePackageManager.APT.install(nativePackage, null);

List<String> commands = result.commands();
String installCommand = commands.getLast();
assertThat(installCommand).contains(debPath);
assertThat(installCommand).doesNotContain("docker-desktop=");
}

@Test
void installFallsBackToPackagesWhenArtifactPathListIsEmpty() {
NativePackage nativePackage = new NativePackage(NativePackageManager.APT, List.of("docker-desktop"), null, null, null, List.of());

PackageManagerCommand result = NativePackageManager.APT.install(nativePackage, null);

List<String> commands = result.commands();
String installCommand = commands.getLast();
assertThat(installCommand).contains("docker-desktop");
}

@Test
void installRejectsMismatchingPackageManager() {
NativePackage nativePackage = new NativePackage(NativePackageManager.APT, List.of("docker-desktop"), null, null, null, List.of());

PackageManagerCommand result = NativePackageManager.APT.install(nativePackage, null);

List<String> commands = result.commands();
String installCommand = commands.getLast();
assertThat(installCommand).contains("docker-desktop");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ void testOfFactoryMethod() {

@Test
void testConstructorWithAllFields() {
NativePackage np = new NativePackage(NativePackageManager.APT, List.of("pkg1"), List.of("--opt"), List.of("setup"), List.of("cleanup"));
NativePackage np = new NativePackage(NativePackageManager.APT, List.of("pkg1"), List.of("--opt"), List.of("setup"), List.of("cleanup"), List.of());

assertThat(np.getExtraInstallOptions()).containsExactly("--opt");
assertThat(np.getSetupCommands()).containsExactly("setup");
Expand All @@ -33,14 +33,14 @@ void testConstructorWithAllFields() {

@Test
void testGetPackages() {
NativePackage np = new NativePackage(NativePackageManager.APT, List.of("pkg1"), null, null, null);
NativePackage np = new NativePackage(NativePackageManager.APT, List.of("pkg1"), null, null, null, null);

assertThat(np.getPackages()).containsExactly("pkg1");
}

@Test
void testNullSafeGetters() {
NativePackage np = new NativePackage(NativePackageManager.APT, List.of("pkg1"), null, null, null);
NativePackage np = new NativePackage(NativePackageManager.APT, List.of("pkg1"), null, null, null, null);

assertThat(np.getExtraInstallOptions()).isEmpty();
assertThat(np.getSetupCommands()).isEmpty();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.devonfw.tools.ide.url.tool.docker;


import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -39,17 +40,14 @@ public String getTool() {
protected void addVersion(UrlVersion urlVersion) {

VersionIdentifier vid = VersionIdentifier.of(urlVersion.getName());
String version = urlVersion.getName().replaceAll("\\.", "");
// get Code for version
String body = doGetResponseBodyAsString(getVersionUrl());
String regex = "href=#" + version
// .......1.........................................................2.................
+ ".{8,12}(\r\n|\r|\n).{0,350}href=https://desktop\\.docker\\.com.*?(\\d{5,6}).*\\.exe";
String regex = "## " + Pattern.quote(urlVersion.getName())
+ ".*?\\[Windows]\\(https://desktop\\.docker\\.com/win/main/amd64/(\\d{5,6})/";
Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
Matcher matcher = pattern.matcher(body);
String code;

if (matcher.find()) {
code = matcher.group(2);
String code = matcher.group(1);
boolean success = doAddVersion(urlVersion,
getDownloadBaseUrl() + "/win/main/amd64/" + code + "/Docker%20Desktop%20Installer.exe", WINDOWS);
if (!success) {
Expand All @@ -58,14 +56,15 @@ protected void addVersion(UrlVersion urlVersion) {
if (WINDOWS_ONLY_VERSIONS.stream().noneMatch(i -> vid.compareVersion(i).isEqual())) {
doAddVersion(urlVersion, getDownloadBaseUrl() + "/mac/main/amd64/" + code + "/Docker.dmg", MAC, X64);
doAddVersion(urlVersion, getDownloadBaseUrl() + "/mac/main/arm64/" + code + "/Docker.dmg", MAC, ARM64);
doAddVersion(urlVersion, getDownloadBaseUrl() + "/linux/main/amd64/" + code + "/docker-desktop-amd64.deb", LINUX);
}
}
}

@Override
protected String getVersionUrl() {

return getVersionBaseUrl() + "/desktop/release-notes/";
return getVersionBaseUrl() + "/desktop/release-notes.md";
}

@Override
Expand Down