feat(v3): Support for Icon Composer Liquid Glass Icons (macOS) - #4934
Conversation
- Check actool version >= 26 requirement - Generate asset.car from Icon Composer input - Validate compilation output and cleanup temp files
- Check if paths are absolute before prepending "./" - Use filepath.Join for temp.plist path construction
Use absolut path, because otherwise we got strange behavior from actool.
WalkthroughAdds support for macOS Liquid Glass icon sources (.icon / Assets.car), macOS-only asset generation via actool, CFBundleIconName propagation through build-assets generate/update flows and Info.plist templates, docs/taskfile/config updates, tests, and conditional bundle copying into app resources. Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as Wails CLI
participant Icons as icons.go
participant Actool as actool (macOS)
participant FS as File System
participant Config as WailsConfig
CLI->>Icons: GenerateIcons(IconComposerInput?, Input?, MacAssetDir?)
Icons->>FS: Check for IconComposerInput / Assets.car / Input files
Icons->>Config: Load/merge WailsConfig (CFBundleIconName?)
Icons->>Icons: checkAndSetCFBundleIconNameCommon()
alt mac asset path (darwin + actool available)
Icons->>Actool: Run actool with IconComposerInput -> MacAssetDir
Actool-->>FS: Emit Assets.car, *.plist, *.icns
FS-->>Icons: Return generated files
Icons->>FS: Parse plist, rename/move *.icns -> darwin/icons.icns
Icons->>Config: Update/persist CFBundleIconName if set
else Non-macOS or unsupported
Icons-->>CLI: Return ErrMacAssetNotSupported or fallback to raster generation
end
alt raster inputs present
Icons->>FS: Generate windows/icon.ico and mac raster icons
end
Icons-->>CLI: Return generated icon artifacts
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@v3/internal/commands/icons_test.go`:
- Around line 127-180: The test is inspecting and deleting "Assets.car" and
"icons.icns" from the current working directory while IconsOptions sets
MacAssetDir to localDir; update the test's test closure to build full paths
using filepath.Join(localDir, "Assets.car") and filepath.Join(localDir,
"icons.icns") (use the same localDir computed in setup), replace os.Stat("...")
and os.Remove("...") calls to use these joined paths, and ensure the cleanup
defer removes the files at those joined paths rather than the bare filenames so
existence checks and cleanup target the MacAssetDir used by
IconComposerInput/MacAssetDir.
In `@v3/internal/commands/icons.go`:
- Around line 61-74: The code currently string-compares the error from
generateMacAsset and sets macIconsGenerated=true even on the non-macOS failure;
change generateMacAsset to return a sentinel exported error (e.g.,
ErrMacAssetNotSupported) and use errors.Is(err, ErrMacAssetNotSupported) to
detect the non-fatal case, set macIconsGenerated = true only when
generateMacAsset returns nil (successful generation), and let other errors
propagate so the Input-based Mac icon generation (IconComposerInput path) can
still run when MacAssetDir was configured but generation was skipped due to
platform support.
🧹 Nitpick comments (7)
v3/internal/commands/build_assets/Taskfile.tmpl.yml (1)
99-109: Consider addingdarwin/Assets.carto thegenerateslist.The task can now produce
Assets.caron macOS when using Icon Composer input, but it's not listed ingenerates(lines 106-107). This may cause the task system to not properly track this output for incremental builds.Note that since Assets.car generation is macOS-only, this is a minor concern as the task will still work correctly—it just won't benefit from task caching for this output on macOS.
💡 Suggested enhancement
generates: - "darwin/icons.icns" - "windows/icon.ico" + - "darwin/Assets.car" cmds:docs/src/content/docs/guides/build/macos.mdx (1)
49-49: Minor: Consider clarifying platform skip behavior.The text says "(skipped on Windows)" but the actual behavior skips on all non-macOS platforms (including Linux). Consider updating for accuracy:
📝 Suggested wording
-This uses `build/appicon.png` to produce `darwin/icons.icns` and `windows/icon.ico`. On macOS you can also provide `build/appicon.icon` (Icon Composer format): the task passes `-iconcomposerinput appicon.icon -macassetdir darwin`, which produces `Assets.car` and `darwin/icons.icns` from the `.icon` file (skipped on Windows). When `Assets.car` is present, run the `update:build-assets` task so that `Info.plist` and `CFBundleIconName` are updated accordingly: +This uses `build/appicon.png` to produce `darwin/icons.icns` and `windows/icon.ico`. On macOS you can also provide `build/appicon.icon` (Icon Composer format): the task passes `-iconcomposerinput appicon.icon -macassetdir darwin`, which produces `Assets.car` and `darwin/icons.icns` from the `.icon` file (skipped on non-macOS platforms). When `Assets.car` is present, run the `update:build-assets` task so that `Info.plist` and `CFBundleIconName` are updated accordingly:v3/internal/commands/build_assets/darwin/Taskfile.yml (1)
164-172: Consider: Theruntask doesn't include Assets.car for dev bundles.The
runtask creates a.dev.appbundle but only copiesicons.icns, notAssets.car. If developers want to test Liquid Glass icons during development, they won't see them in the dev app bundle.This may be intentional to keep dev builds simple, but it's worth documenting or adding the same conditional copy for consistency.
💡 Optional: Add Assets.car to dev bundle
run: cmds: - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + fi - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"v3/internal/commands/build-assets.go (2)
151-156: Misleading comment: code runs between extractions, not before.The comment states "This must happen BEFORE template extraction" but this code executes after the first extraction (line 147) and before the second extraction (line 162). The logic works correctly because CFBundleIconName is used in
updatable_build_assetstemplates, but the comment is confusing.📝 Clarify the comment
- // Check if Assets.car exists - if so, set CFBundleIconName if not already set - // This must happen BEFORE template extraction so CFBundleIconName is available in the template + // Check if Assets.car exists - if so, set CFBundleIconName if not already set + // This must happen BEFORE the updatable_build_assets extraction so CFBundleIconName is available in Info.plist templates checkAndSetCFBundleIconName(options.Dir, options, &config)
305-335: Near-duplicate helper functions could be unified.
checkAndSetCFBundleIconNameandcheckAndSetCFBundleIconNameUpdatehave identical logic with different option/config types. Consider using an interface or extracting the common pattern.♻️ Example: extract common logic
// CFBundleIconNameSetter interface for types that can set CFBundleIconName type CFBundleIconNameSetter interface { GetCFBundleIconName() string SetCFBundleIconName(string) } func checkAndSetCFBundleIconNameCommon(dir string, setter CFBundleIconNameSetter) { darwinDir := filepath.Join(dir, "darwin") assetsCarPath := filepath.Join(darwinDir, "Assets.car") if _, err := os.Stat(assetsCarPath); err == nil { if setter.GetCFBundleIconName() == "" { setter.SetCFBundleIconName("appicon") } } }v3/internal/commands/icons.go (2)
221-234: Consider capturing stderr for better actool error diagnostics.When
actoolfails, only the error is returned. Capturing stderr would provide more useful diagnostic information.🔧 Proposed improvement to capture stderr
cmd = exec.Command("actool", iconComposerPath, "--compile", macAssetDirPath, "--notices", "--warnings", "--errors", - "--output-partial-info-plist", filepath.Join(macAssetDirPath, "/temp.plist"), + "--output-partial-info-plist", filepath.Join(macAssetDirPath, "temp.plist"), "--app-icon", iconComposerFilename, "--enable-on-demand-resources", "NO", "--development-region", "en", "--target-device", "mac", "--minimum-deployment-target", "26.0", "--platform", "macosx") - out, err := cmd.Output() + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = cmd.Run() if err != nil { - return fmt.Errorf("failed to run actool: %w", err) + return fmt.Errorf("failed to run actool: %w\nstderr: %s", err, stderr.String()) } + out := stdout.Bytes()Note: Also removed the leading slash from
"/temp.plist"for consistency.
253-255: Hardcoded file count expectation is brittle.Requiring exactly 3 output files may break if actool behavior changes. Consider verifying required files exist without enforcing exact count.
♻️ Proposed flexible validation
- if len(outputFiles) != 3 { - return fmt.Errorf("expected 3 output files, got %d", len(outputFiles)) - } - // Check that we have one .car file and one .plist file var carFile, plistFile, icnsFile string for _, file := range outputFiles { filePath, ok := file.(string) if !ok { return fmt.Errorf("output file is not a string: %v", file) } ext := filepath.Ext(filePath) switch ext { case ".car": carFile = filePath case ".plist": plistFile = filePath case ".icns": icnsFile = filePath - default: - return fmt.Errorf("unexpected output file extension: %s", ext) + // Ignore other output files that may be added in future actool versions } }
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@v3/internal/commands/icons.go`:
- Around line 221-225: The call building the actool command uses
filepath.Join(macAssetDirPath, "/temp.plist") which ignores macAssetDirPath due
to the leading slash; update the argument used when constructing the
exec.Command (the code around cmd = exec.Command(...) where
"--output-partial-info-plist" is set) to join the directory correctly (e.g.,
filepath.Join(macAssetDirPath, "temp.plist") or otherwise construct an absolute
path without a leading slash) so the plist is written inside macAssetDirPath
rather than to the filesystem root.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@v3/internal/commands/build_assets/darwin/Taskfile.yml`:
- Around line 141-147: The current Taskfile always tries to copy
build/darwin/icons.icns which fails for Assets.car-only bundles; update the
packaging steps around the cp of build/darwin/icons.icns (and the duplicate
block at the later similar location) to check for files first: if
build/darwin/icons.icns exists, cp it into
"{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"; else if
build/darwin/Assets.car exists, skip the icon copy (Assets.car is sufficient);
otherwise, fail with a clear error that neither icons.icns nor Assets.car were
found. Ensure this conditional logic is applied to both the earlier block
(around the cp of icons.icns and Assets.car) and the later block referenced at
lines 166-172.
In `@v3/internal/commands/build-assets.go`:
- Around line 246-248: The code unconditionally overwrites
options.CFBundleIconName with wailsConfig.Info.CFBundleIconName which ignores
CLI flags; change the assignment to only set options.CFBundleIconName when it is
empty (follow the same pattern used for other fields) by checking
options.CFBundleIconName == "" before assigning from
wailsConfig.Info.CFBundleIconName so CLI-provided values take precedence.
In `@v3/internal/commands/icons.go`:
- Around line 66-79: The current logic in icons.go silently ignores
ErrMacAssetNotSupported when options.IconComposerInput is set but no fallback
output (e.g., options.Input) exists, causing a no-op success; update the
generate path that calls generateMacAsset(options) to detect when
IconComposerInput != "" and no other output target is provided and, instead of
swallowing ErrMacAssetNotSupported, return that error (or wrap it with context)
so callers see the unsupported-platform failure; specifically change the block
around generateMacAsset(options) and the macIconsGenerated handling to return
ErrMacAssetNotSupported (or a wrapped error) when generateMacAsset returns that
error and no fallback generation will run.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@v3/internal/commands/icons.go`:
- Around line 217-240: The mac asset output directory (macAssetDirPath) must be
created before invoking actool; before the line that sets cmd =
exec.Command(...), call os.MkdirAll(macAssetDirPath, 0o755) (or appropriate
permissions) and handle any error (return fmt.Errorf(...)) so actool --compile
won't fail; ensure this creation occurs after computing macAssetDirPath and
before exec.Command is called.
|
|
Thank you for such a well put together PR 🙏 It was an opportunity to also get the cross-compilation tests setup in CI so double win! 🎉 That icon is 😍 |
…app#4934) * feat(icons): implement Mac asset (.car) generation with actool - Check actool version >= 26 requirement - Generate asset.car from Icon Composer input - Validate compilation output and cleanup temp files * Wails Icon as Icon Composer file * a generated assets.car from the wails icon * handle absolute paths correctly in actool command - Check if paths are absolute before prepending "./" - Use filepath.Join for temp.plist path construction * add test for Assets.car generation * Skipping Asset.car generation and test on non mac-systems * add CFBundleIconName generation to plist, if Assets.car exists * also create .icns from .icon-File and use always absolut path Use absolut path, because otherwise we got strange behavior from actool. * update to use appicon as CFBundleIconName and optionally use the name from config * update the Taskfiles * remove log prints * the awesome new LiquidGlass icon files * update doc * Update UNRELEASED_CHANGELOG.md * Update UNRELEASED_CHANGELOG.md * fix security bug * Skip icon generation test with actool on CI * fix error from coderabbitai * solved the coderabbitai nitpicks * fix coderabbitai findings * Update changelog --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com>



Description
It adds support for Icon Composer (.icon) and asset catalogs (Assets.car) for macOS, enabling Wails3 apps to use the modern vector-based, Liquid Glass-style app icon format. It also includes a beautiful Wails Liquid Glass icon that greets the user right away when a new project is created and adapts to the default light, dark or clear variants.
Changes
Icon generation (wails3 generate icons)
-iconcomposerinput(path to .icon file) and-macassetdir(output directory for macOS assets)Build assets & Info.plist
build/darwin/Assets.carexists,build-assets.gosets CFBundleIconName in Info.plistcfBundleIconName; if omitted and Assets.car exists, it defaults to "appicon"Other
Tests
Fixes #4909
Type of change
Please select the option that is relevant.
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration using
wails doctor.If you checked Linux, please specify the distro and version.
Test Configuration
Checklist:
website/src/pages/changelog.mdxwith details of this PRSummary by CodeRabbit
New Features
Documentation
Chores
Tests
✏️ Tip: You can customize this high-level summary in your review settings.