Skip to content

Commit 6bd309a

Browse files
fvarruiclaude
andcommitted
Retry hdiutil commands when generating DMG
hdiutil can fail transiently with "Resource busy" while macOS scans the new image, which is common on CI runners. Retry each hdiutil command up to 3 times before failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c673b59 commit 6bd309a

2 files changed

Lines changed: 40 additions & 5 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# DMG generation fails intermittently with "Resource busy"
2+
3+
## Problem
4+
5+
DMG generation sometimes fails with `hdiutil: create failed - Resource busy`. The rest of the build succeeds, but no DMG is produced. Seen in 1 of 7 macOS smoke test jobs (run 37165552012, `macos-14 / ARM64`). Common on CI runners, where the system scans new disk images.
6+
7+
## Cause
8+
9+
`hdiutil` can fail transiently while macOS (Spotlight, XProtect) holds the image or volume. `GenerateDmg` ran each `hdiutil` command once and gave up on the first error.
10+
11+
## Solution
12+
13+
`GenerateDmg` runs every `hdiutil` command (`create`, `attach`, `detach`, `convert`) through a private `hdiutil(...)` helper. It retries up to 3 times, waiting 5 s and then 10 s, and logs a warning for each retry. After the last attempt the original error is thrown as before.
14+
15+
## Verification
16+
17+
- The failure is intermittent, so it can't be reproduced on demand.
18+
- The macOS smoke tests must stay green. If the error shows up again, the build log must show the `hdiutil ... failed (attempt n of 3), retrying` warning, followed by a DMG.

‎src/main/java/io/github/fvarrui/javapackager/packagers/GenerateDmg.java‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
*/
2323
public class GenerateDmg extends ArtifactGenerator<MacPackager> {
2424

25+
private static final int HDIUTIL_ATTEMPTS = 3;
26+
2527
public GenerateDmg() {
2628
super("DMG image");
2729
}
@@ -90,16 +92,16 @@ protected File doApply(MacPackager packager) throws Exception {
9092
boolean isAarch64 = osArchitecture.equalsIgnoreCase("aarch64");
9193
String fileSystem = isAarch64 ? "APFS" : "HFS+";
9294
Logger.warn(osArchitecture + " architecture detected. Using " + fileSystem + " filesystem");
93-
execute("hdiutil", "create", "-srcfolder", appFolder, "-volname", volumeName, "-ov", "-fs", fileSystem, "-format", "UDRW", tempDmgFile);
95+
hdiutil("create", "-srcfolder", appFolder, "-volname", volumeName, "-ov", "-fs", fileSystem, "-format", "UDRW", tempDmgFile);
9496

9597
if (mountFolder.exists()) {
9698
Logger.info("Unmounting volume: " + mountFolder);
97-
execute("hdiutil", "detach", mountFolder);
99+
hdiutil("detach", mountFolder);
98100
}
99101

100102
// mounts image
101103
Logger.info("Mounting image: " + tempDmgFile.getAbsolutePath());
102-
String result = execute("hdiutil", "attach", "-readwrite", "-noverify", "-noautoopen", tempDmgFile);
104+
String result = hdiutil("attach", "-readwrite", "-noverify", "-noautoopen", tempDmgFile);
103105
Optional<String> optDeviceName = Arrays.stream(result.split("\n"))
104106
.filter(s -> s.contains(mountFolder.getAbsolutePath()))
105107
.map(StringUtils::normalizeSpace)
@@ -146,11 +148,11 @@ protected File doApply(MacPackager packager) throws Exception {
146148

147149
// unmounts
148150
Logger.info("Unmounting volume: " + mountFolder);
149-
execute("hdiutil", "detach", mountFolder);
151+
hdiutil("detach", mountFolder);
150152

151153
// compress image
152154
Logger.info("Compressing disk image...");
153-
execute("hdiutil", "convert", tempDmgFile, "-ov", "-format", "UDZO", "-imagekey", "zlib-level=9", "-o", dmgFile);
155+
hdiutil("convert", tempDmgFile, "-ov", "-format", "UDZO", "-imagekey", "zlib-level=9", "-o", dmgFile);
154156
tempDmgFile.delete();
155157

156158
// checks if dmg file was created
@@ -160,5 +162,20 @@ protected File doApply(MacPackager packager) throws Exception {
160162

161163
return dmgFile;
162164
}
165+
166+
/**
167+
* Runs hdiutil, retrying if it fails (e.g. "Resource busy" while the image is being scanned)
168+
*/
169+
private String hdiutil(Object... arguments) throws Exception {
170+
for (int attempt = 1; ; attempt++) {
171+
try {
172+
return execute("hdiutil", arguments);
173+
} catch (Exception e) {
174+
if (attempt == HDIUTIL_ATTEMPTS) throw e;
175+
Logger.warn("hdiutil " + arguments[0] + " failed (attempt " + attempt + " of " + HDIUTIL_ATTEMPTS + "), retrying: " + e.getMessage());
176+
ThreadUtils.sleep(attempt * 5000L);
177+
}
178+
}
179+
}
163180

164181
}

0 commit comments

Comments
 (0)