Module: Plushie::Widget::NativeBuild
- Defined in:
- lib/plushie/widget/native_build.rb
Overview
Native widget build pipeline.
Writes a virtual app crate under +_build/plushie-renderer-spec/+,
lists the configured native widget crates as path dependencies,
and delegates the actual workspace generation and cargo build
to +cargo plushie build+. The built binary is then copied into
+_build/plushie/bin/+ where the renderer discovery chain expects
to find it.
All of the heavy lifting (workspace generation, [patch.crates-io] forwarding, version-skew detection, collision checks, constructor validation) lives in cargo-plushie so every host SDK shares one implementation.
Constant Summary collapse
- SCRATCH_DIR =
Directory holding the generated virtual app crate that cargo-plushie reads via cargo_metadata.
File.join("_build", "plushie-renderer-spec")
Class Method Summary collapse
-
.build_with_widgets(widgets, release: false, verbose: false, bin_name: nil, **_) ⇒ String
Build the renderer binary.
-
.clean! ⇒ Object
Remove the build workspace and compiled artifacts.
-
.configured_widgets ⇒ Array<Class>
Returns native widget classes from configuration.
-
.filter_native(classes) ⇒ Array<Class>
Filter to native-only widgets, skipping non-native with a warning.
-
.find_built_binary(target_root, bin_name, profile, ext) ⇒ String?
Search only within cargo's target root for a profile-matching binary if cargo-plushie's generated workspace layout changes.
-
.install_binary(src) ⇒ String
Install the built binary under +_build/plushie/bin/+ using the platform-suffixed name so the renderer discovery chain finds it.
-
.invoke_cargo_plushie(scratch, release:, verbose:)
Shell out to cargo-plushie, passing the scratch manifest path.
-
.locate_built_binary(scratch, bin_name, release) ⇒ String
Find the binary cargo-plushie produced.
-
.manifest_path(scratch) ⇒ String
Absolute path to the virtual app manifest we hand cargo-plushie.
-
.resolve_crate_paths(widgets, base_dir: Dir.pwd) ⇒ Hash{Class => String}
Resolve crate paths with directory traversal security check.
-
.verify_widget_metadata!(crate_paths)
Verify each widget crate declares the metadata table cargo-plushie looks for.
-
.widget_metadata?(content) ⇒ Boolean
Cheap TOML sniffer that avoids pulling in a parser.
-
.write_virtual_manifest(scratch, bin_name, crate_paths)
Write the virtual app Cargo.toml.
Class Method Details
.build_with_widgets(widgets, release: false, verbose: false, bin_name: nil, **_) ⇒ String
Build the renderer binary. Works for stock builds (no native widgets) and custom builds (one or more native widgets).
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/plushie/widget/native_build.rb', line 105 def (, release: false, verbose: false, bin_name: nil, **_) bin_name ||= if .empty? "plushie-renderer" else ENV["PLUSHIE_BUILD_NAME"] || Plushie.configuration.build_name end crate_paths = resolve_crate_paths() (crate_paths) scratch = File.(SCRATCH_DIR) FileUtils.mkdir_p(scratch) write_virtual_manifest(scratch, bin_name, crate_paths) puts "Widgets: #{.map(&:name).join(", ")}" if .any? invoke_cargo_plushie(scratch, release: release, verbose: verbose) binary_src = locate_built_binary(scratch, bin_name, release) unless File.exist?(binary_src) raise Error, "Build succeeded but binary not found at #{binary_src}" end install_binary(binary_src) end |
.clean! ⇒ Object
Remove the build workspace and compiled artifacts.
321 322 323 324 325 326 327 328 329 330 |
# File 'lib/plushie/widget/native_build.rb', line 321 def clean! removed = false [File.join("_build", "plushie"), SCRATCH_DIR].each do |dir| next unless File.directory?(dir) FileUtils.rm_rf(dir) puts "Removed #{dir}" removed = true end puts "Nothing to clean" unless removed end |
.configured_widgets ⇒ Array<Class>
Returns native widget classes from configuration.
Reads from (in priority order):
- Plushie.configuration.widgets (set via Plushie.configure block)
- PLUSHIE_WIDGETS env var (comma-separated class names, for CI)
Non-native widgets in the list are skipped with a warning.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/plushie/widget/native_build.rb', line 38 def from_config = Plushie.configuration. return filter_native(from_config) if from_config.is_a?(Array) && from_config.any? env = ENV["PLUSHIE_WIDGETS"] || ENV["PLUSHIE_EXTENSIONS"] return [] unless env && !env.strip.empty? names = env.split(",").map(&:strip).reject(&:empty?) classes = names.map do |name| Object.const_get(name) rescue NameError raise Error, "Widget class '#{name}' specified in PLUSHIE_WIDGETS could not be found. " \ "Ensure the class is defined and the file is required before running the build." end filter_native(classes) end |
.filter_native(classes) ⇒ Array<Class>
Filter to native-only widgets, skipping non-native with a warning.
59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/plushie/widget/native_build.rb', line 59 def filter_native(classes) classes.each { |mod| mod.finalize! if mod.respond_to?(:finalize!) } classes.select do |mod| if mod.respond_to?(:native?) && mod.native? true else warn "plushie: skipping #{mod.name} (not a native_widget)" false end end end |
.find_built_binary(target_root, bin_name, profile, ext) ⇒ String?
Search only within cargo's target root for a profile-matching binary if cargo-plushie's generated workspace layout changes.
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/plushie/widget/native_build.rb', line 280 def find_built_binary(target_root, bin_name, profile, ext) return nil unless File.directory?(target_root) expected_name = "#{bin_name}#{ext}" matches = [] Find.find(target_root) do |path| next unless File.file?(path) next unless File.basename(path) == expected_name next unless File.basename(File.dirname(path)) == profile matches << path end return nil if matches.empty? return matches.first if matches.length == 1 raise Error, "multiple built binaries named #{expected_name.inspect} found under #{target_root.inspect}" end |
.install_binary(src) ⇒ String
Install the built binary under +_build/plushie/bin/+ using the platform-suffixed name so the renderer discovery chain finds it.
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'lib/plushie/widget/native_build.rb', line 303 def install_binary(src) bin_file = ENV["PLUSHIE_BIN_FILE"] || Plushie.configuration.bin_file if bin_file dest = bin_file FileUtils.mkdir_p(File.dirname(dest)) else dest_dir = File.join("_build", "plushie", "bin") FileUtils.mkdir_p(dest_dir) dest = File.join(dest_dir, Plushie::Binary.binary_name) end FileUtils.cp(src, dest) File.chmod(0o755, dest) puts "Installed to #{dest}" dest end |
.invoke_cargo_plushie(scratch, release:, verbose:)
This method returns an undefined value.
Shell out to cargo-plushie, passing the scratch manifest path.
238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/plushie/widget/native_build.rb', line 238 def invoke_cargo_plushie(scratch, release:, verbose:) program, preamble = Plushie::CargoPlushie.resolve args = preamble + ["build", "--manifest-path", manifest_path(scratch)] args << "--release" if release args << "--verbose" if verbose puts "Running: #{program} #{args.join(" ")}" if verbose ok = system(program, *args) return if ok raise Error, "cargo plushie build failed" end |
.locate_built_binary(scratch, bin_name, release) ⇒ String
Find the binary cargo-plushie produced. cargo-plushie writes
the generated renderer workspace under +
261 262 263 264 265 266 267 268 269 270 |
# File 'lib/plushie/widget/native_build.rb', line 261 def locate_built_binary(scratch, bin_name, release) target_root = ENV["CARGO_TARGET_DIR"] || File.join(scratch, "target") profile = release ? "release" : "debug" ext = Gem.win_platform? ? ".exe" : "" preferred = File.join(target_root, "plushie-renderer", "target", profile, "#{bin_name}#{ext}") return preferred if File.exist?(preferred) discovered = find_built_binary(target_root, bin_name, profile, ext) discovered || preferred end |
.manifest_path(scratch) ⇒ String
Absolute path to the virtual app manifest we hand cargo-plushie.
135 136 137 |
# File 'lib/plushie/widget/native_build.rb', line 135 def manifest_path(scratch) File.join(scratch, "Cargo.toml") end |
.resolve_crate_paths(widgets, base_dir: Dir.pwd) ⇒ Hash{Class => String}
Resolve crate paths with directory traversal security check.
Host SDKs are allowed to declare widget crates by relative path. We refuse paths that escape the project root so a malicious widget declaration can't point cargo at arbitrary filesystem locations.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/plushie/widget/native_build.rb', line 81 def resolve_crate_paths(, base_dir: Dir.pwd) .each_with_object({}) do |mod, paths| rel = mod.native_crate resolved = File.(File.join(base_dir, rel)) allowed = File.(base_dir) unless resolved.start_with?("#{allowed}/") || resolved == allowed raise Error, "Widget #{mod.name} native_crate path #{rel.inspect} " \ "resolves to #{resolved}, which is outside the allowed directory #{allowed}" end paths[mod] = resolved end end |
.verify_widget_metadata!(crate_paths)
This method returns an undefined value.
Verify each widget crate declares the metadata table cargo-plushie looks for. cargo-plushie will also complain, but failing here produces a message that references the Ruby widget class name (far more useful than a cargo_metadata dump).
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/plushie/widget/native_build.rb', line 146 def (crate_paths) crate_paths.each do |mod, crate_path| unless File.directory?(crate_path) raise Error, "widget #{mod.name} crate directory not found at #{crate_path}. " \ "Check the rust_crate path configuration." end toml_path = File.join(crate_path, "Cargo.toml") unless File.exist?(toml_path) raise Error, "widget #{mod.name} crate at #{crate_path} is missing Cargo.toml" end content = File.read(toml_path) next if (content) raise Error, "widget #{mod.name} crate #{crate_path} is missing " \ "[package.metadata.plushie.widget] { type_name, constructor }. " \ "Add that table so cargo plushie build can discover the widget." end end |
.widget_metadata?(content) ⇒ Boolean
Cheap TOML sniffer that avoids pulling in a parser. Looks for the +[package.metadata.plushie.widget]+ header plus the two required keys somewhere after it.
176 177 178 179 180 181 182 183 184 185 |
# File 'lib/plushie/widget/native_build.rb', line 176 def (content) header = /^\[package\.metadata\.plushie\.widget\]/.match(content) return false unless header rest = content.byteslice(header.end(0), content.bytesize) || "" # Stop at the next section header so we only consider keys # inside the widget table. body = rest.split(/^\[/, 2).first || "" body.match?(/^\s*type_name\s*=/) && body.match?(/^\s*constructor\s*=/) end |
.write_virtual_manifest(scratch, bin_name, crate_paths)
This method returns an undefined value.
Write the virtual app Cargo.toml. The app package has no source
code of its own; it exists so cargo-plushie's cargo metadata
walk finds the widget crates as direct dependencies and the
[package.metadata.plushie] table supplies the binary name.
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/plushie/widget/native_build.rb', line 196 def write_virtual_manifest(scratch, bin_name, crate_paths) package_name = bin_name.tr("-", "_") dep_lines = crate_paths.values.map do |path| name = File.basename(path) %(#{name} = { path = "#{path}" }) end content = <<~TOML # Auto-generated by rake plushie:build. Do not edit. # # This virtual manifest exists only so `cargo plushie build` # can discover the native widget crates via cargo_metadata. [package] name = "#{package_name}" version = "0.0.0" edition = "2024" publish = false [lib] path = "src/lib.rs" [dependencies] #{dep_lines.join("\n")} [package.metadata.plushie] binary_name = "#{bin_name}" TOML FileUtils.mkdir_p(File.join(scratch, "src")) File.write(File.join(scratch, "src", "lib.rs"), "") File.write(manifest_path(scratch), content) end |