• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 26477587146

26 May 2026 09:59PM UTC coverage: 70.991% (+0.02%) from 70.971%
26477587146

Pull #1970

github

web-flow
Merge af6938c52 into d93123de5
Pull Request #1970: Feature/1846 skip xattr for non app installs

4467 of 6970 branches covered (64.09%)

Branch coverage included in aggregate %.

11577 of 15630 relevant lines covered (74.07%)

3.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.31
cli/src/main/java/com/devonfw/tools/ide/os/MacOsHelper.java
1
package com.devonfw.tools.ide.os;
2

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.Iterator;
7
import java.util.Set;
8
import java.util.stream.Stream;
9

10
import org.slf4j.Logger;
11
import org.slf4j.LoggerFactory;
12

13
import com.devonfw.tools.ide.context.IdeContext;
14
import com.devonfw.tools.ide.io.FileAccess;
15
import com.devonfw.tools.ide.process.ProcessErrorHandling;
16
import com.devonfw.tools.ide.process.ProcessMode;
17
import com.devonfw.tools.ide.process.ProcessResult;
18
import com.devonfw.tools.ide.tool.ToolCommandlet;
19
import com.devonfw.tools.ide.tool.repository.ToolRepository;
20

21
/**
22
 * Internal helper class for MacOS workarounds.
23
 */
24
public final class MacOsHelper {
25

26
  private static final Logger LOG = LoggerFactory.getLogger(MacOsHelper.class);
3✔
27

28
  private static final Set<String> INVALID_LINK_FOLDERS = Set.of(IdeContext.FOLDER_CONTENTS,
6✔
29
      IdeContext.FOLDER_RESOURCES, IdeContext.FOLDER_BIN);
30

31
  private final IdeContext context;
32

33
  private final FileAccess fileAccess;
34

35
  private final SystemInfo systemInfo;
36

37
  /**
38
   * The constructor.
39
   *
40
   * @param context the {@link IdeContext} instance.
41
   */
42
  public MacOsHelper(IdeContext context) {
43

44
    super();
2✔
45
    this.context = context;
3✔
46
    this.fileAccess = context.getFileAccess();
4✔
47
    this.systemInfo = context.getSystemInfo();
4✔
48
  }
1✔
49

50
  /**
51
   * The constructor.
52
   *
53
   * @param fileAccess the {@link FileAccess} instance.
54
   * @param systemInfo the {@link SystemInfo} instance.
55
   */
56
  public MacOsHelper(FileAccess fileAccess, SystemInfo systemInfo) {
57

58
    super();
2✔
59
    this.context = null;
3✔
60
    this.fileAccess = fileAccess;
3✔
61
    this.systemInfo = systemInfo;
3✔
62
  }
1✔
63

64
  /**
65
   * Fixes macOS Gatekeeper blocking for downloaded tools. On macOS 15.1+ (Apple Silicon), just removing {@code com.apple.quarantine} is not enough since
66
   * unsigned apps still get the "is damaged" error. So we clear all xattrs first, then ad-hoc codesign any {@code .app} bundles. Call this after writing
67
   * {@code .ide.software.version} since codesigning seals the bundle.
68
   * <p>
69
   * No-op for installations that do not contain a {@code .app} bundle (e.g. tarball/CLI tools like the JDK): Gatekeeper does not block those, and running
70
   * {@code xattr -cr} unnecessarily produces noisy warnings on locked-down corporate Macs where the operation is not permitted (see issue #1846).
71
   *
72
   * @param path the {@link Path} to the installation directory.
73
   */
74
  public void removeQuarantineAttribute(Path path) {
75

76
    if (!this.systemInfo.isMac()) {
4✔
77
      return;
1✔
78
    }
79
    if (this.context == null) {
3!
80
      LOG.debug("Cannot fix Gatekeeper for {} - no context available", path);
×
81
      return;
×
82
    }
83
    Path appDir = findAppDir(path);
4✔
84
    if (appDir == null) {
2✔
85
      LOG.debug("No .app bundle found in {} - skipping Gatekeeper workaround", path);
4✔
86
      return;
1✔
87
    }
88
    // clear all extended attributes (quarantine, resource forks, etc.) before codesigning
89
    LOG.debug("Clearing extended attributes from {}", path);
4✔
90
    try {
91
      this.context.newProcess().executable("xattr").addArgs("-cr", path).run(ProcessMode.DEFAULT_SILENT);
×
92
    } catch (Exception e) {
1✔
93
      LOG.warn("Could not clear extended attributes from {}: {}", path, e.getMessage(), e);
18✔
94
    }
×
95
    // ad-hoc codesign .app bundles only if they are not already properly signed (e.g. Eclipse is notarized - we must not replace that)
96
    try {
97
      ProcessResult verifyResult = this.context.newProcess().executable("codesign")
6✔
98
          .errorHandling(ProcessErrorHandling.NONE)
11✔
99
          .addArgs("-v", appDir).run(ProcessMode.DEFAULT_SILENT);
×
100
      if (!verifyResult.isSuccessful()) {
×
101
        LOG.debug("Ad-hoc codesigning {}", appDir);
×
102
        ProcessResult signResult = this.context.newProcess().executable("codesign")
×
103
            .errorHandling(ProcessErrorHandling.LOG_WARNING)
×
104
            .addArgs("--force", "--deep", "--sign", "-", appDir).run(ProcessMode.DEFAULT_SILENT);
×
105
        if (!signResult.isSuccessful()) {
×
106
          LOG.warn("Could not codesign {} - app may be blocked by Gatekeeper", appDir);
×
107
        }
108
      }
109
    } catch (Exception e) {
1✔
110
      LOG.warn("Codesign not available for {}: {}", appDir, e.getMessage(), e);
18✔
111
    }
×
112
  }
1✔
113

114
  /**
115
   * @param rootDir the {@link Path} to the root directory.
116
   * @return the path to the app directory.
117
   */
118
  public Path findAppDir(Path rootDir) {
119
    return this.fileAccess.findFirst(rootDir,
7✔
120
        p -> p.getFileName().toString().endsWith(".app") && Files.isDirectory(p), false);
15!
121
  }
122

123
  /**
124
   * @param rootDir the {@link Path} to the root directory.
125
   * @param tool the name of the tool to find the link directory for.
126
   * @return the {@link com.devonfw.tools.ide.tool.ToolInstallation#linkDir() link directory}.
127
   */
128
  public Path findLinkDir(Path rootDir, String tool) {
129

130
    if (!this.systemInfo.isMac() || Files.isDirectory(rootDir.resolve(IdeContext.FOLDER_BIN))) {
11✔
131
      return rootDir;
2✔
132
    }
133
    Path contentsDir = rootDir.resolve(IdeContext.FOLDER_CONTENTS);
4✔
134
    if (Files.isDirectory(contentsDir)) {
5✔
135
      return findLinkDir(contentsDir, rootDir, tool);
6✔
136
    }
137
    Path appDir = findAppDir(rootDir);
4✔
138
    if (appDir != null) {
2✔
139
      contentsDir = appDir.resolve(IdeContext.FOLDER_CONTENTS);
4✔
140
      if (Files.isDirectory(contentsDir)) {
5!
141
        return findLinkDir(contentsDir, rootDir, tool);
6✔
142
      }
143
    }
144
    return rootDir;
2✔
145
  }
146

147
  /**
148
   * Finds the root tool path of a tool in MacOS
149
   *
150
   * @param commandlet the {@link ToolCommandlet}
151
   * @param context the {@link IdeContext}
152
   * @return a {@link String}
153
   */
154
  public Path findRootToolPath(ToolCommandlet commandlet, IdeContext context) {
155
    return context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT).resolve(commandlet.getName())
9✔
156
        .resolve(commandlet.getInstalledEdition())
3✔
157
        .resolve(commandlet.getInstalledVersion().toString());
3✔
158
  }
159

160
  private Path findLinkDir(Path contentsDir, Path rootDir, String tool) {
161

162
    LOG.debug("Found MacOS app in {}", contentsDir);
4✔
163
    Path resourcesAppBin = contentsDir.resolve(IdeContext.FOLDER_RESOURCES).resolve(IdeContext.FOLDER_APP)
6✔
164
        .resolve(IdeContext.FOLDER_BIN);
2✔
165
    if (Files.isDirectory(resourcesAppBin)) {
5✔
166
      return resourcesAppBin.getParent();
3✔
167
    }
168
    Path linkDir = findContentSubfolder(contentsDir, tool);
5✔
169
    if (linkDir != null) {
2!
170
      return linkDir;
2✔
171
    }
172
    return rootDir;
×
173
  }
174

175
  private Path findContentSubfolder(Path dir, String tool) {
176

177
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
178
      Iterator<Path> iterator = childStream.iterator();
3✔
179
      while (iterator.hasNext()) {
3!
180
        Path child = iterator.next();
4✔
181
        String filename = child.getFileName().toString();
4✔
182
        if (INVALID_LINK_FOLDERS.contains(filename) || filename.startsWith("_")) {
8✔
183
          continue;
1✔
184
        } else if (Files.isDirectory(child.resolve(IdeContext.FOLDER_BIN)) || Files.exists(child.resolve(tool))) {
14✔
185
          return child;
4✔
186
        }
187
      }
1✔
188
    } catch (IOException e) {
4!
189
      throw new IllegalStateException("Failed to search for file in " + dir, e);
×
190
    }
×
191
    return null;
×
192
  }
193

194
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc