Class: Everywhere::DesktopDevApp

Inherits:
Object
  • Object
show all
Defined in:
lib/everywhere/desktop_dev_app.rb

Overview

What every dev stages under ~/.rubyeverywhere so a live cargo run shell looks like the app being built: the flat assets folder, the Dock icon, and on macOS a throwaway .app around the dev binary — plus the command that links the two together and launches it.

None of this is packaging. every build writes the real, complete bundle; this is the subset a dev session needs, restaged on every launch so editing an asset and pressing d is enough to see it.

Instance Method Summary collapse

Constructor Details

#initialize(config:) ⇒ DesktopDevApp

Returns a new instance of DesktopDevApp.



22
23
24
# File 'lib/everywhere/desktop_dev_app.rb', line 22

def initialize(config:)
  @config = config
end

Instance Method Details

#desktop_command(prepared) ⇒ Object

macOS runs the shell from inside a throwaway .app so it has a name; everywhere else cargo run is still the whole story.

Three steps in one command rather than three Ruby calls, because the link has to happen between the compile and the launch and we want ONE supervised child: cargo's progress relays into the dock, and exec then replaces it with the app, so the pid we're tracking is the window.

A hard link, not a copy: same volume (both under ~/.rubyeverywhere), so it's free and instant, where copying a debug Tauri binary is ~100MB on every launch. cargo replaces the file on rebuild rather than writing through it, so the link is remade each time — hence ln -f.



125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/everywhere/desktop_dev_app.rb', line 125

def desktop_command(prepared)
  return "cargo run" unless Host.darwin?

  _app, exe = stage_dev_bundle
  built = File.join(prepared.target_dir, "debug", "example-shell")
  [
    "cargo build",
    "{ ln -f #{built.shellescape} #{exe.shellescape} 2>/dev/null || " \
      "cp -f #{built.shellescape} #{exe.shellescape}; }",
    "exec #{exe.shellescape}"
  ].join(" && ")
end

#dev_bundle_nameObject

Display names are free-form (spaces, punctuation, emoji all fine), but this one becomes a directory name — so the separators, and a leading dot that would hide or escape the bundle, are the only things taken out.



61
62
63
64
# File 'lib/everywhere/desktop_dev_app.rb', line 61

def dev_bundle_name
  cleaned = @config.name.to_s.gsub(%r{[/\\]}, "-").sub(/\A\.+/, "").strip
  cleaned.empty? ? "App" : cleaned
end

#dev_exe_nameObject

Spaces are legal in CFBundleExecutable but a nuisance in every shell command that names it, so the executable follows the packaged app's convention and drops them.



69
# File 'lib/everywhere/desktop_dev_app.rb', line 69

def dev_exe_name = dev_bundle_name.gsub(/\s+/, "")

#dev_info_plistObject



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/everywhere/desktop_dev_app.rb', line 71

def dev_info_plist
  name = Everywhere::Plist.escape(@config.name)
  <<~PLIST
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>CFBundleName</key><string>#{name}</string>
      <key>CFBundleDisplayName</key><string>#{name}</string>
      <key>CFBundleExecutable</key><string>#{Everywhere::Plist.escape(dev_exe_name)}</string>
      <key>CFBundleIdentifier</key><string>#{Everywhere::Plist.escape(@config.bundle_id)}</string>
      <key>CFBundlePackageType</key><string>APPL</string>
      <key>CFBundleShortVersionString</key><string>#{Everywhere::Plist.escape(@config.version)}</string>
      <key>NSHighResolutionCapable</key><true/>
    </dict>
    </plist>
  PLIST
end

#stage_desktop_assetsObject

native/desktop/assets/** → the staged flat folder the running shell reads. Runs the same compiler every build does, so an asset that resolves in dev resolves in the packaged app too. Returns [dir, errors]; the dir is returned even when the app ships no assets, because the shell only has to cope with it not existing.



31
32
33
34
35
36
# File 'lib/everywhere/desktop_dev_app.rb', line 31

def stage_desktop_assets
  dest = Everywhere::Paths.desktop_assets_dir(@config.bundle_id)
  source = File.join(@config.root, "native", "desktop", "assets")
  _names, errors = Everywhere::DesktopAssets.write!(source, dest)
  [dest, errors]
end

#stage_desktop_iconObject

The Dock icon for the running shell. cargo run produces a bare binary with no .app around it, so macOS has nothing to read an icon from and Tauri's compiled-in placeholder wins — which is how you end up staring at the gem's icon while testing your own app.

Shaped with the same Big Sur inset + squircle the packaged .icns gets (Icon.macos_master), so the Dock looks identical in dev and in a release build rather than subtly rounder in one of them. Returns "" when the app has no icon — the shell reads that as "leave it alone".



99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/everywhere/desktop_dev_app.rb', line 99

def stage_desktop_icon
  source = @config.icon
  return "" unless source && File.exist?(source)

  dest = File.join(Everywhere::Paths.desktop_icon_dir(@config.bundle_id), "icon.png")
  FileUtils.mkdir_p(File.dirname(dest))
  FileUtils.cp(source, dest) unless Everywhere::Icon.macos_master(source, dest)
  dest
rescue StandardError => e
  # A Dock icon is never worth failing a dev session over.
  UI.warn("couldn't prepare the Dock icon (#{e.class}) — using the default")
  ""
end

#stage_dev_bundleObject

A minimal .app around the dev binary, so macOS has a CFBundleName to read. Without it every menu that names the app says "example-shell" — the crate name — because an unbundled process has nothing else to go on. (Setting NSProcessInfo's processName at runtime does NOT fix this: the menu bar and the predefined About/Hide/Quit items are built from the bundle, and macOS has already decided by the time any Rust runs.)

Only the plist matters here. No icon is stamped: the shell sets the Dock icon at runtime from NATIVE_ICON_PATH, because Tauri applies its own compiled-in icon after launch and would win over a bundle icns anyway. every build writes the real, complete plist — this is the dev subset.



49
50
51
52
53
54
55
56
# File 'lib/everywhere/desktop_dev_app.rb', line 49

def stage_dev_bundle
  app = File.join(Everywhere::Paths.desktop_dev_app_dir(@config.bundle_id),
                  "#{dev_bundle_name}.app")
  macos = File.join(app, "Contents", "MacOS")
  FileUtils.mkdir_p(macos)
  File.write(File.join(app, "Contents", "Info.plist"), dev_info_plist)
  [app, File.join(macos, dev_exe_name)]
end