Module: Ruflet::CLI::BuildCommand

Includes:
AndroidSdk, EnvironmentSetup, FlutterSdk
Included in:
Ruflet::CLI
Defined in:
lib/ruflet/cli/build_command.rb

Constant Summary collapse

CLIENT_EXTENSION_MAP =
{
  "ads" => { package: "flet_ads", alias: "ruflet_ads" },
  "audio" => { package: "flet_audio", alias: "ruflet_audio" },
  "audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
  "camera" => { package: "flet_camera", alias: "ruflet_camera" },
  "charts" => { package: "flet_charts", alias: "ruflet_charts" },
  "code_editor" => { package: "flet_code_editor", alias: "ruflet_code_editor" },
  "color_pickers" => { package: "flet_color_pickers", alias: "ruflet_color_picker" },
  "datatable2" => { package: "flet_datatable2", alias: "ruflet_datatable2" },
  "flashlight" => { package: "flet_flashlight", alias: "ruflet_flashlight" },
  "geolocator" => { package: "flet_geolocator", alias: "ruflet_geolocator" },
  "lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
  "map" => { package: "flet_map", alias: "ruflet_map" },
  "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
  "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
  "video" => { package: "flet_video", alias: "ruflet_video" },
  "webview" => { package: "flet_webview", alias: "ruflet_webview" }
}.freeze
SERVICE_EXTENSION_MAP =
{
  "camera" => %w[camera permission_handler],
  "microphone" => %w[audio_recorder permission_handler],
  "location" => %w[geolocator permission_handler],
  "motion" => %w[permission_handler]
}.freeze
DEFAULT_SERVICE_NATIVE_REQUIREMENTS =
{
  "camera" => {
    android_permissions: ["android.permission.CAMERA"],
    ios_info: {
      "NSCameraUsageDescription" => "Camera access is required for camera experiences."
    },
    macos_info: {
      "NSCameraUsageDescription" => "Camera access is required for camera experiences."
    },
    macos_entitlements: {
      "com.apple.security.device.camera" => true
    },
    ios_permission_definitions: %w[
      PERMISSION_CAMERA=1
    ]
  },
  "microphone" => {
    android_permissions: ["android.permission.RECORD_AUDIO"],
    ios_info: {
      "NSMicrophoneUsageDescription" => "Microphone access is required for audio recording."
    },
    macos_info: {
      "NSMicrophoneUsageDescription" => "Microphone access is required for audio recording."
    },
    macos_entitlements: {
      "com.apple.security.device.audio-input" => true
    },
    ios_permission_definitions: %w[
      PERMISSION_MICROPHONE=1
    ]
  },
  "motion" => {
    ios_info: {
      "NSMotionUsageDescription" => "Motion access is required for motion and sensor readings."
    },
    ios_permission_definitions: %w[
      PERMISSION_SENSORS=1
    ]
  },
  "location" => {
    android_permissions: [
      "android.permission.ACCESS_FINE_LOCATION",
      "android.permission.ACCESS_COARSE_LOCATION"
    ],
    ios_info: {
      "NSLocationWhenInUseUsageDescription" => "Location access is required for location-aware experiences."
    },
    macos_info: {
      "NSLocationUsageDescription" => "Location access is required for location-aware experiences."
    },
    macos_entitlements: {
      "com.apple.security.personal-information.location" => true
    },
    ios_permission_definitions: %w[
      PERMISSION_LOCATION=1
    ]
  }
}.freeze

Constants included from AndroidSdk

AndroidSdk::ANDROID_BUILD_TOOLS, AndroidSdk::ANDROID_PLATFORM, AndroidSdk::CMDLINE_TOOLS_BASE, AndroidSdk::CMDLINE_TOOLS_VERSION, AndroidSdk::JDK_PACKAGES, AndroidSdk::MINIMUM_JAVA_MAJOR

Constants included from EnvironmentSetup

EnvironmentSetup::LINUX_PACKAGE_MANAGERS

Constants included from FlutterSdk

FlutterSdk::DEFAULT_FLUTTER_CHANNEL, FlutterSdk::RELEASES_BASE

Instance Method Summary collapse

Methods included from AndroidSdk

#android_build_env, #android_environment_setup!, #detect_android_sdk_root, #managed_android_sdk_root

Methods included from EnvironmentSetup

#environment_setup!, #required_system_tools, #system_package_manager

Methods included from FlutterSdk

#ensure_flutter!, #flutter_version_summary

Instance Method Details

#command_build(args) ⇒ Object



102
103
104
# File 'lib/ruflet/cli/build_command.rb', line 102

def command_build(args)
  with_project_build_lock { run_build_command(args) }
end

#command_install(args) ⇒ Object



195
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
230
# File 'lib/ruflet/cli/build_command.rb', line 195

def command_install(args)
  verbose = args.delete("--verbose") || args.delete("-v")
  device_id = extract_option_value!(args, "--device", "-d")

  client_dir = ensure_flutter_client_dir(verbose: !!verbose)
  unless client_dir
    warn "Could not find Flutter client directory."
    warn "Set RUFLET_CLIENT_DIR or let Ruflet manage the client under ./build/client"
    return 1
  end

  tools = ensure_flutter!("install", client_dir: client_dir)
  command_env = install_tool_env(tools[:env], client_dir)
  install_platform = install_platform_for_device(device_id)
  unless sync_built_outputs_for_install(client_dir, platform: install_platform, verbose: !!verbose)
    warn "Could not find built app outputs under ./build"
    warn "Run `ruflet build ...` first, then `ruflet install`."
    return 1
  end
  unless validate_install_artifacts(client_dir, platform: install_platform, device_id: device_id)
    return 1
  end

  install_args = ["install"]
  install_args += ["-d", device_id] if device_id
  install_args << "-v" if verbose

  build_log(verbose, "client_dir=#{client_dir}")
  build_log(verbose, "flutter=#{tools[:flutter]}")
  build_log(verbose, "dart=#{tools[:dart]}")
  build_log(verbose, "install_command=#{([tools[:flutter]] + install_args).join(' ')}")
  build_note("Installing app#{device_id ? " to device #{device_id}" : ""}")

  ok = run_external_command(command_env, tools[:flutter], *install_args, chdir: client_dir, unbundled: true)
  ok ? 0 : 1
end

#run_build_command(args) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/ruflet/cli/build_command.rb', line 128

def run_build_command(args)
  self_contained = args.delete("--self")
  verbose = args.delete("--verbose") || args.delete("-v")
  platform = (args.shift || "").downcase
  if platform.empty?
    warn "Usage: ruflet build <apk|android|ios|aab|web|macos|windows|linux> [--self] [--verbose]"
    return 1
  end

  flutter_cmd = flutter_build_command(platform)
  unless flutter_cmd
    warn "Unsupported build target: #{platform}"
    return 1
  end

  ensure_ruflet_build_assets(verbose: !!verbose)
  client_dir = ensure_flutter_client_dir(verbose: !!verbose)
  unless client_dir
    warn "Could not find Flutter client directory."
    warn "Set RUFLET_CLIENT_DIR or let Ruflet manage the client under ./build/client"
    return 1
  end

  build_note("Preparing #{platform} build (#{self_contained ? 'self-contained' : 'server-driven'})")
  config = load_ruflet_config
  tools = ensure_flutter!("build", client_dir: client_dir)
  command_env = build_tool_env(tools[:env], platform, client_dir)
  ok = prepare_flutter_client(
    client_dir,
    platform: platform,
    tools: tools.merge(env: command_env),
    config: config,
    self_contained: !!self_contained,
    verbose: !!verbose
  )
  return 1 unless ok

  build_args = [*flutter_cmd, *args]
  build_args << "--codesign" if ios_device_build_needs_codesign_flag?(platform, build_args)
  target_entrypoint = flutter_target_entrypoint(client_dir, self_contained: !!self_contained)
  build_args += ["--target", target_entrypoint] if target_entrypoint
  backend_url = configured_backend_url(config)
  if self_contained
    build_args += ["--dart-define", "RUFLET_BACKEND_URL=#{backend_url}"] if backend_url
  else
    unless backend_url
      warn "build config error: backend_url is required for server-driven builds"
      warn "Set app.backend_url or backend_url in ruflet.yaml"
      return 1
    end
    build_args += ["--dart-define", "RUFLET_BACKEND_URL=#{backend_url}"]
  end
  build_args << "-v" if verbose

  build_log(verbose, "mode=#{self_contained ? 'self' : 'server'}")
  build_log(verbose, "client_dir=#{client_dir}")
  build_log(verbose, "flutter=#{tools[:flutter]}")
  build_log(verbose, "dart=#{tools[:dart]}")
  build_log(verbose, "target=#{target_entrypoint}") if target_entrypoint
  build_log(verbose, "command=#{([tools[:flutter]] + build_args).join(' ')}")

  build_note("Running Flutter #{build_args.join(' ')}")
  ok = run_external_command(command_env, tools[:flutter], *build_args, chdir: client_dir, unbundled: true)
  export_platform_build_outputs(client_dir, platform, verbose: !!verbose) if ok
  ok ? 0 : 1
end

#with_project_build_lockObject

Concurrent builds share build/client and corrupt each other’s state (missing app.dill, Xcode build.db I/O errors). flock is released automatically when the process exits, so the lock cannot go stale.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/ruflet/cli/build_command.rb', line 109

def with_project_build_lock
  lock_dir = File.join(Dir.pwd, "build")
  FileUtils.mkdir_p(lock_dir)
  lock_path = File.join(lock_dir, ".ruflet_build.lock")
  File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |file|
    unless file.flock(File::LOCK_EX | File::LOCK_NB)
      owner = file.read.to_s.strip
      warn "Another ruflet build is already running for this project#{owner.empty? ? '' : " (#{owner})"}."
      warn "Concurrent builds share the same build directory and corrupt each other."
      warn "Wait for it to finish, then retry."
      return 1
    end
    file.truncate(0)
    file.write("pid=#{Process.pid} started=#{Time.now}")
    file.flush
    yield
  end
end