ruflet 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Ruflet
6
+ module CLI
7
+ # Provisions the host so `ruflet build` can run: checks (and with
8
+ # `ruflet doctor --fix`, installs) the system tools Flutter requires.
9
+ #
10
+ # Coverage:
11
+ # Linux apt (Ubuntu, Debian, Kali, Mint), dnf (Fedora, RHEL),
12
+ # pacman (Arch, Omarchy, Manjaro), zypper (openSUSE)
13
+ # macOS Homebrew where available, plus Xcode Command Line Tools and
14
+ # CocoaPods guidance
15
+ # Windows winget or Chocolatey
16
+ #
17
+ # Anything that cannot be installed unattended (Xcode CLT, Visual Studio
18
+ # Build Tools) is reported with the exact command the developer must run.
19
+ module EnvironmentSetup
20
+ Tool = Struct.new(:name, :probe, :purpose, :packages, :manual_hint, keyword_init: true)
21
+
22
+ LINUX_PACKAGE_MANAGERS = [
23
+ { id: :apt, probe: "apt-get", install: %w[apt-get install -y], update: %w[apt-get update] },
24
+ { id: :dnf, probe: "dnf", install: %w[dnf install -y], update: nil },
25
+ { id: :pacman, probe: "pacman", install: %w[pacman -S --noconfirm --needed], update: nil },
26
+ { id: :zypper, probe: "zypper", install: %w[zypper --non-interactive install], update: nil },
27
+ { id: :apk, probe: "apk", install: %w[apk add], update: nil }
28
+ ].freeze
29
+
30
+ def environment_setup!(fix: false, verbose: false)
31
+ issues = []
32
+ tools = required_system_tools
33
+
34
+ unless flutter_host
35
+ issues << unsupported_host_message
36
+ warn " System: #{unsupported_host_message}"
37
+ return issues
38
+ end
39
+
40
+ manager = system_package_manager
41
+ missing = tools.reject { |tool| tool_present?(tool) }
42
+
43
+ if missing.empty?
44
+ puts " System tools: ok (#{tools.map(&:name).join(', ')})"
45
+ return issues
46
+ end
47
+
48
+ puts " System tools missing: #{missing.map(&:name).join(', ')}"
49
+
50
+ unless fix
51
+ missing.each { |tool| warn " #{tool.name}: #{manual_hint_for(tool, manager)}" }
52
+ warn " Run `ruflet doctor --fix` to install them."
53
+ return missing.map { |tool| "missing #{tool.name}" }
54
+ end
55
+
56
+ auto, manual = missing.partition { |tool| installable?(tool, manager) }
57
+
58
+ if auto.any?
59
+ if manager
60
+ install_system_packages(manager, auto, verbose: verbose)
61
+ end
62
+ still_missing = auto.reject { |tool| tool_present?(tool) }
63
+ still_missing.each do |tool|
64
+ issues << "missing #{tool.name}"
65
+ warn " #{tool.name}: install failed — #{manual_hint_for(tool, manager)}"
66
+ end
67
+ installed = auto - still_missing
68
+ puts " Installed: #{installed.map(&:name).join(', ')}" if installed.any?
69
+ end
70
+
71
+ manual.each do |tool|
72
+ issues << "missing #{tool.name}"
73
+ warn " #{tool.name}: requires a manual step — #{manual_hint_for(tool, manager)}"
74
+ end
75
+
76
+ issues
77
+ end
78
+
79
+ def required_system_tools
80
+ case flutter_host
81
+ when "linux" then linux_required_tools
82
+ when "macos", "macos_arm64" then macos_required_tools
83
+ when "windows" then windows_required_tools
84
+ else []
85
+ end
86
+ end
87
+
88
+ def system_package_manager
89
+ if windows_host?
90
+ return { id: :winget, install: %w[winget install --accept-source-agreements --accept-package-agreements -e --id] } if which_command("winget")
91
+ return { id: :choco, install: %w[choco install -y] } if which_command("choco")
92
+ return nil
93
+ end
94
+
95
+ if macos_host?
96
+ return { id: :brew, install: %w[brew install] } if which_command("brew")
97
+ return nil
98
+ end
99
+
100
+ LINUX_PACKAGE_MANAGERS.each do |manager|
101
+ next unless which_command(manager[:probe])
102
+
103
+ return manager
104
+ end
105
+ nil
106
+ end
107
+
108
+ private
109
+
110
+ def macos_host?
111
+ RbConfig::CONFIG["host_os"].match?(/darwin/i)
112
+ end
113
+
114
+ def linux_required_tools
115
+ [
116
+ Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
117
+ packages: { apt: "git", dnf: "git", pacman: "git", zypper: "git", apk: "git" }),
118
+ Tool.new(name: "curl", probe: "curl", purpose: "SDK downloads",
119
+ packages: { apt: "curl", dnf: "curl", pacman: "curl", zypper: "curl", apk: "curl" }),
120
+ Tool.new(name: "unzip", probe: "unzip", purpose: "extracting Flutter artifacts",
121
+ packages: { apt: "unzip", dnf: "unzip", pacman: "unzip", zypper: "unzip", apk: "unzip" }),
122
+ Tool.new(name: "xz", probe: "xz", purpose: "extracting the Flutter SDK (.tar.xz)",
123
+ packages: { apt: "xz-utils", dnf: "xz", pacman: "xz", zypper: "xz", apk: "xz" }),
124
+ Tool.new(name: "zip", probe: "zip", purpose: "packaging app bundles",
125
+ packages: { apt: "zip", dnf: "zip", pacman: "zip", zypper: "zip", apk: "zip" }),
126
+ Tool.new(name: "clang", probe: "clang", purpose: "Linux desktop builds",
127
+ packages: { apt: "clang", dnf: "clang", pacman: "clang", zypper: "clang", apk: "clang" }),
128
+ Tool.new(name: "cmake", probe: "cmake", purpose: "Linux desktop builds",
129
+ packages: { apt: "cmake", dnf: "cmake", pacman: "cmake", zypper: "cmake", apk: "cmake" }),
130
+ Tool.new(name: "ninja", probe: "ninja", purpose: "Linux desktop builds",
131
+ packages: { apt: "ninja-build", dnf: "ninja-build", pacman: "ninja", zypper: "ninja", apk: "ninja" }),
132
+ Tool.new(name: "pkg-config", probe: "pkg-config", purpose: "Linux desktop builds",
133
+ packages: { apt: "pkg-config", dnf: "pkgconf-pkg-config", pacman: "pkgconf", zypper: "pkg-config", apk: "pkgconf" }),
134
+ Tool.new(name: "GTK 3 headers", probe: nil, purpose: "Linux desktop builds",
135
+ packages: { apt: "libgtk-3-dev", dnf: "gtk3-devel", pacman: "gtk3", zypper: "gtk3-devel", apk: "gtk+3.0-dev" })
136
+ ]
137
+ end
138
+
139
+ def macos_required_tools
140
+ [
141
+ Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
142
+ packages: { brew: "git" },
143
+ manual_hint: "install Xcode Command Line Tools: xcode-select --install"),
144
+ Tool.new(name: "curl", probe: "curl", purpose: "SDK downloads", packages: { brew: "curl" }),
145
+ Tool.new(name: "unzip", probe: "unzip", purpose: "extracting Flutter artifacts", packages: { brew: "unzip" }),
146
+ Tool.new(name: "Xcode Command Line Tools", probe: nil, purpose: "macOS/iOS builds",
147
+ packages: {},
148
+ manual_hint: "run `xcode-select --install` (or install Xcode from the App Store)"),
149
+ Tool.new(name: "CocoaPods", probe: "pod", purpose: "macOS/iOS plugin dependencies",
150
+ packages: { brew: "cocoapods" },
151
+ manual_hint: "run `brew install cocoapods` or `sudo gem install cocoapods`")
152
+ ]
153
+ end
154
+
155
+ def windows_required_tools
156
+ [
157
+ Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
158
+ packages: { winget: "Git.Git", choco: "git" }),
159
+ Tool.new(name: "Visual Studio Build Tools", probe: nil, purpose: "Windows desktop builds",
160
+ packages: {},
161
+ manual_hint: "winget install --id Microsoft.VisualStudio.2022.BuildTools (select the " \
162
+ "\"Desktop development with C++\" workload)")
163
+ ]
164
+ end
165
+
166
+ def tool_present?(tool)
167
+ case tool.name
168
+ when "GTK 3 headers"
169
+ return true if which_command("pkg-config") &&
170
+ system("pkg-config", "--exists", "gtk+-3.0", out: File::NULL, err: File::NULL)
171
+
172
+ false
173
+ when "Xcode Command Line Tools"
174
+ system("xcode-select", "-p", out: File::NULL, err: File::NULL)
175
+ else
176
+ tool.probe ? !which_command(tool.probe).nil? : false
177
+ end
178
+ end
179
+
180
+ def installable?(tool, manager)
181
+ return false unless manager
182
+
183
+ tool.packages.key?(manager[:id])
184
+ end
185
+
186
+ def manual_hint_for(tool, manager)
187
+ return tool.manual_hint if tool.manual_hint && !installable?(tool, manager)
188
+ return "no supported package manager found; install #{tool.name} manually" unless manager
189
+
190
+ package = tool.packages[manager[:id]]
191
+ return tool.manual_hint || "install #{tool.name} manually" unless package
192
+
193
+ "#{(sudo_prefix + manager[:install]).join(' ')} #{package}"
194
+ end
195
+
196
+ def install_system_packages(manager, tools, verbose: false)
197
+ packages = tools.map { |tool| tool.packages[manager[:id]] }.compact.uniq
198
+ return if packages.empty?
199
+
200
+ if manager[:update]
201
+ run_privileged_command(*manager[:update], verbose: verbose)
202
+ end
203
+
204
+ if manager[:id] == :winget
205
+ # winget installs one id at a time.
206
+ packages.each { |package| run_privileged_command(*manager[:install], package, verbose: verbose) }
207
+ else
208
+ run_privileged_command(*manager[:install], *packages, verbose: verbose)
209
+ end
210
+ end
211
+
212
+ def run_privileged_command(*command, verbose: false)
213
+ full = sudo_prefix + command
214
+ puts " $ #{full.join(' ')}"
215
+ # Package installs (apt/dnf/pacman...) routinely download hundreds of
216
+ # megabytes and take minutes. Always stream their output so the user
217
+ # can see progress instead of a frozen-looking prompt; `verbose` is
218
+ # left for callers that want to add their own detail elsewhere.
219
+ system(*full, out: $stdout, err: $stderr)
220
+ end
221
+
222
+ def sudo_prefix
223
+ return [] if windows_host? || macos_host?
224
+ return [] if Process.respond_to?(:uid) && Process.uid.zero?
225
+ return ["sudo"] if which_command("sudo")
226
+
227
+ []
228
+ end
229
+
230
+ def unsupported_host_message
231
+ os = RbConfig::CONFIG["host_os"]
232
+ if os.match?(/linux/i) && machine_arch.match?(/arm|aarch64/)
233
+ "Flutter publishes no official Linux #{machine_arch} build; use an x64 machine " \
234
+ "or build Flutter from source (https://github.com/flutter/flutter)."
235
+ else
236
+ "unsupported host platform: #{os}"
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -6,6 +6,8 @@ module Ruflet
6
6
  module CLI
7
7
  module ExtraCommand
8
8
  include FlutterSdk
9
+ include EnvironmentSetup
10
+ include AndroidSdk
9
11
  include NewCommand
10
12
 
11
13
  def command_create(args)
@@ -20,6 +22,12 @@ module Ruflet
20
22
  puts "Ruflet doctor"
21
23
  puts " Ruby: #{RUBY_VERSION}"
22
24
  puts " Flutter host target: #{flutter_host || 'unsupported'}"
25
+
26
+ # System prerequisites first: Flutter cannot be installed (or even
27
+ # extracted) without them.
28
+ environment_issues = environment_setup!(fix: !!fix, verbose: !!verbose)
29
+ return 1 unless flutter_host
30
+
23
31
  if template_root
24
32
  puts " Template: #{template_root}"
25
33
  elsif fix
@@ -46,9 +54,13 @@ module Ruflet
46
54
  end
47
55
  end
48
56
  puts " Flutter: #{flutter_version_summary(tools)}"
49
- ok = system(tools[:env], tools[:flutter], "doctor", *(verbose ? ["-v"] : []))
50
- status = $?.exitstatus if $?
51
- status ||= ok ? 0 : 1
57
+ environment_issues += android_environment_setup!(fix: !!fix, verbose: !!verbose)
58
+ ok = system(android_build_env(tools[:env]), tools[:flutter], "doctor", *(verbose ? ["-v"] : []))
59
+ status = ok ? 0 : ($?&.exitstatus || 1)
60
+ if environment_issues.any?
61
+ warn "Unresolved environment issues: #{environment_issues.join('; ')}"
62
+ status = 1 if status.zero?
63
+ end
52
64
  status
53
65
  end
54
66
 
@@ -84,8 +84,12 @@ module Ruflet
84
84
  File.write(fvmrc_path, "{\"flutter\":\"#{version}\"}\n")
85
85
  end
86
86
 
87
- system(fvm_env, fvm, "install", version.to_s, chdir: project_dir, out: File::NULL, err: File::NULL)
88
- system(fvm_env, fvm, "use", "--force", version.to_s, chdir: project_dir, out: File::NULL, err: File::NULL)
87
+ # Installing a Flutter SDK through FVM downloads/extracts hundreds of
88
+ # megabytes. Stream FVM's output so the run does not look frozen.
89
+ puts " $ fvm install #{version}"
90
+ system(fvm_env, fvm, "install", version.to_s, chdir: project_dir, out: $stdout, err: $stderr)
91
+ puts " $ fvm use --force #{version}"
92
+ system(fvm_env, fvm, "use", "--force", version.to_s, chdir: project_dir, out: $stdout, err: $stderr)
89
93
 
90
94
  flutter = flutter_bin_path(project_dir)
91
95
  return nil unless File.executable?(flutter)
@@ -107,7 +111,8 @@ module Ruflet
107
111
  end
108
112
  return nil unless dart && File.executable?(dart)
109
113
 
110
- system(dart, "pub", "global", "activate", "fvm", out: File::NULL, err: File::NULL)
114
+ puts " $ dart pub global activate fvm"
115
+ system(dart, "pub", "global", "activate", "fvm", out: $stdout, err: $stderr)
111
116
  installed_fvm = File.join(pub_cache_bin_dir, fvm_executable_name)
112
117
  return installed_fvm if File.executable?(installed_fvm)
113
118
 
@@ -148,10 +153,13 @@ module Ruflet
148
153
  return sdk_root if File.executable?(flutter_bin)
149
154
 
150
155
  FileUtils.mkdir_p(install_root)
156
+ puts " Installing Flutter #{release.fetch('version')} (#{host}) into #{install_root}"
151
157
  Dir.mktmpdir("ruflet-flutter-sdk-") do |tmpdir|
152
158
  archive_path = File.join(tmpdir, File.basename(archive))
153
- download_file("#{RELEASES_BASE}/#{archive}", archive_path)
159
+ download_file("#{RELEASES_BASE}/#{archive}", archive_path, label: "Flutter SDK")
160
+ puts " Extracting #{File.basename(archive)} (this can take a minute)"
154
161
  extract_archive(archive_path, install_root)
162
+ puts " Extracted Flutter SDK"
155
163
  end
156
164
 
157
165
  return sdk_root if File.executable?(flutter_bin)
@@ -322,7 +330,10 @@ module Ruflet
322
330
  if os.match?(/darwin/i)
323
331
  return machine_arch.include?("arm") ? "macos_arm64" : "macos"
324
332
  end
325
- return "linux" if os.match?(/linux/i)
333
+ if os.match?(/linux/i)
334
+ # Flutter publishes Linux archives for x64 only.
335
+ return machine_arch.match?(/arm|aarch64/) ? nil : "linux"
336
+ end
326
337
  return "windows" if os.match?(/mswin|mingw|cygwin/i)
327
338
 
328
339
  nil
@@ -363,7 +374,7 @@ module Ruflet
363
374
  nil
364
375
  end
365
376
 
366
- def download_file(url, destination, limit: 5)
377
+ def download_file(url, destination, limit: 5, label: nil)
367
378
  raise "Too many redirects while downloading #{url}" if limit <= 0
368
379
 
369
380
  uri = URI(url)
@@ -373,10 +384,21 @@ module Ruflet
373
384
  http.request(req) do |res|
374
385
  case res
375
386
  when Net::HTTPSuccess
376
- File.open(destination, "wb") { |f| res.read_body { |chunk| f.write(chunk) } }
387
+ name = label || File.basename(destination)
388
+ total = res["content-length"].to_i
389
+ downloaded = 0
390
+ marker = -1
391
+ File.open(destination, "wb") do |f|
392
+ res.read_body do |chunk|
393
+ f.write(chunk)
394
+ downloaded += chunk.bytesize
395
+ marker = report_download_progress(name, downloaded, total, marker)
396
+ end
397
+ end
398
+ finish_download_progress(name, downloaded, total)
377
399
  return destination
378
400
  when Net::HTTPRedirection
379
- return download_file(res["location"], destination, limit: limit - 1)
401
+ return download_file(res["location"], destination, limit: limit - 1, label: label)
380
402
  else
381
403
  raise "Download failed (#{res.code})"
382
404
  end
@@ -386,19 +408,75 @@ module Ruflet
386
408
  curl_download(url, destination)
387
409
  end
388
410
 
411
+ # Net::HTTP gives no progress for a multi-hundred-MB SDK download, which
412
+ # looks like a hang. Report percentage on a TTY (rewritten in place) and
413
+ # at coarse steps when piped, so logs stay readable either way.
414
+ def report_download_progress(name, downloaded, total, marker)
415
+ if total.positive?
416
+ percent = downloaded * 100 / total
417
+ return marker if percent <= marker
418
+
419
+ line = " Downloading #{name}: #{percent}% (#{human_size(downloaded)} / #{human_size(total)})"
420
+ if $stdout.tty?
421
+ $stdout.print("\r#{line}")
422
+ elsif (percent % 10).zero?
423
+ puts line
424
+ end
425
+ percent
426
+ else
427
+ step = downloaded / (5 * 1024 * 1024)
428
+ return marker if step <= marker
429
+
430
+ line = " Downloading #{name}: #{human_size(downloaded)}"
431
+ $stdout.tty? ? $stdout.print("\r#{line}") : puts(line)
432
+ step
433
+ end
434
+ end
435
+
436
+ def finish_download_progress(name, downloaded, total)
437
+ summary = total.positive? ? "#{human_size(downloaded)} / #{human_size(total)}" : human_size(downloaded)
438
+ if $stdout.tty?
439
+ $stdout.print("\r Downloaded #{name}: #{summary}\n")
440
+ else
441
+ puts " Downloaded #{name}: #{summary}"
442
+ end
443
+ end
444
+
445
+ def human_size(bytes)
446
+ units = %w[B KB MB GB TB]
447
+ size = bytes.to_f
448
+ unit = units.shift
449
+ while size >= 1024 && units.any?
450
+ size /= 1024
451
+ unit = units.shift
452
+ end
453
+ format("%.1f %s", size, unit)
454
+ end
455
+
389
456
  def extract_archive(archive, destination)
390
457
  if archive.end_with?(".zip")
391
458
  if windows_host?
392
459
  return system("powershell", "-NoProfile", "-Command", "Expand-Archive -Path '#{archive}' -DestinationPath '#{destination}' -Force")
393
460
  end
461
+
462
+ require_extract_tool!("unzip")
394
463
  return system("unzip", "-oq", archive, "-d", destination)
395
464
  end
396
465
 
397
466
  if archive.end_with?(".tar.xz") || archive.end_with?(".tar.gz") || archive.end_with?(".tgz")
467
+ require_extract_tool!("tar")
468
+ require_extract_tool!("xz") if archive.end_with?(".tar.xz") && !windows_host?
398
469
  return system("tar", "-xf", archive, "-C", destination)
399
470
  end
400
471
 
401
- false
472
+ raise "Unsupported archive format: #{File.basename(archive)}"
473
+ end
474
+
475
+ def require_extract_tool!(name)
476
+ return if which_command(name)
477
+
478
+ raise "`#{name}` is required to extract the Flutter SDK but was not found. " \
479
+ "Run `ruflet doctor --fix` to install the missing system tools."
402
480
  end
403
481
  end
404
482
  end
@@ -25,6 +25,7 @@ module Ruflet
25
25
  "map" => { package: "flet_map", alias: "ruflet_map" },
26
26
  "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
27
27
  "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
28
+ "spinkit" => { package: "flet_spinkit", alias: "ruflet_spinkit" },
28
29
  "video" => { package: "flet_video", alias: "ruflet_video" },
29
30
  "webview" => { package: "flet_webview", alias: "ruflet_webview" }
30
31
  }.freeze
@@ -84,7 +85,20 @@ module Ruflet
84
85
  end
85
86
 
86
87
  cached_template = cached_ruflet_client_template_root
87
- return cached_template if Dir.exist?(cached_template)
88
+ if Dir.exist?(cached_template)
89
+ return cached_template if cached_template_current?(cached_template)
90
+
91
+ # The cache was written by a different ruflet version; stale
92
+ # framework Dart breaks newer asset layouts. Refresh, but never
93
+ # block a build on the network: fall back to the stale copy loudly.
94
+ refreshed = download_ruflet_template(force: true)
95
+ return refreshed if refreshed && Dir.exist?(refreshed)
96
+
97
+ warn "ruflet: could not refresh the Flutter client template cache; " \
98
+ "using a stale copy from another ruflet version at #{cached_template}. " \
99
+ "Delete it or run with network access to update."
100
+ return cached_template
101
+ end
88
102
 
89
103
  [
90
104
  File.expand_path("../../../ruflet_client", __dir__),
@@ -112,6 +126,21 @@ module Ruflet
112
126
  File.join(template_cache_root, "ruflet_flutter_template")
113
127
  end
114
128
 
129
+ TEMPLATE_CACHE_STAMP = ".ruflet_template_version"
130
+
131
+ def cached_template_current?(target)
132
+ stamp = File.join(target, TEMPLATE_CACHE_STAMP)
133
+ File.file?(stamp) && File.read(stamp).strip == Ruflet::VERSION
134
+ rescue StandardError
135
+ false
136
+ end
137
+
138
+ def write_template_cache_stamp(target)
139
+ File.write(File.join(target, TEMPLATE_CACHE_STAMP), Ruflet::VERSION)
140
+ rescue StandardError
141
+ nil
142
+ end
143
+
115
144
  def cached_ruby_runtime_root
116
145
  File.join(cache_root, "ruby_runtime")
117
146
  end
@@ -150,6 +179,7 @@ module Ruflet
150
179
  FileUtils.cp_r(source, target)
151
180
  end
152
181
 
182
+ write_template_cache_stamp(target)
153
183
  target
154
184
  rescue StandardError => e
155
185
  warn "Failed to fetch Ruflet template: #{e.class}: #{e.message}"
@@ -195,12 +225,11 @@ module Ruflet
195
225
  # Example: https://api.example.com
196
226
  backend_url: ""
197
227
 
198
- # Source of truth for Flutter client extensions/plugins.
199
- # To test every extension, list all services:
200
- # ads, audio, audio_recorder, camera, charts, code_editor, color_pickers,
201
- # datatable2, flashlight, geolocator, lottie, map, permission_handler,
202
- # secure_storage, video, webview
203
- services: []
228
+ # Flutter extension packages included in the managed client.
229
+ # Permission-backed extensions such as camera, audio_recorder,
230
+ # geolocator, and permission_handler are activated by services.yaml
231
+ # and ignored here.
232
+ extensions: []
204
233
 
205
234
  # Build assets configuration consumed by `ruflet build`.
206
235
  # Paths are relative to this file unless absolute.
@@ -215,6 +244,11 @@ module Ruflet
215
244
  icon_background: "#FFFFFF"
216
245
  theme_color: "#FFFFFF"
217
246
  YAML
247
+
248
+ File.write(File.join(root, "services.yaml"), <<~YAML)
249
+ # Protected device access requested by this app.
250
+ services: []
251
+ YAML
218
252
  end
219
253
 
220
254
  def copy_default_project_assets(root)