From c5df09d7eeae924d34558e26d63c82cf4576469b Mon Sep 17 00:00:00 2001 From: Joel Winarske Date: Thu, 18 Jun 2026 08:29:14 -0700 Subject: [PATCH] fix(env): make setup_env.sh follow the -w workspace emb flutter now emits setup_env.sh into the resolved workspace after a successful install, so FLUTTER_WORKSPACE matches the -w target instead of the cwd a later bare `emb env` would default to. emb env creates the output dir before writing (so `emb env -w ` no longer crashes on a missing dir) and warns when /flutter is absent. Its help and README are narrowed to its real role - regenerate after a move and --print - since setup/flutter emit the file on the happy path. Signed-off-by: Joel Winarske --- README.md | 25 +++++-- lib/src/commands/env_command.dart | 29 ++++++-- lib/src/commands/flutter_command.dart | 14 ++++ test/src/commands/env_command_test.dart | 50 ++++++++++++++ test/src/commands/flutter_command_test.dart | 75 +++++++++++++++++++++ 5 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 test/src/commands/env_command_test.dart create mode 100644 test/src/commands/flutter_command_test.dart diff --git a/README.md b/README.md index a3e1624..67dfe91 100644 --- a/README.md +++ b/README.md @@ -215,17 +215,20 @@ emb sync --config ../configs -j 8 ### `emb flutter` -Install the Flutter SDK into `/flutter`. +Install the Flutter SDK into `/flutter`, then emit +`/setup_env.sh` so `FLUTTER_WORKSPACE` (and the SDK's Flutter/Dart on +`PATH`) match the workspace just provisioned — i.e. the `-w` target. | Option | Default | Description | |---|---|---| -| `-w`, `--workspace ` | resolution order | Workspace root. | +| `-w`, `--workspace ` | resolution order | Workspace root. Also becomes `FLUTTER_WORKSPACE` in the emitted `setup_env.sh`. | | `--flutter-version ` | `globals.json` `flutter_version` | Version/tag/branch to check out. | | `-c`, `--config ` | `configs` | Directory to read `globals.json` from. | | `--configure` | off | Run `flutter config` (desktop + custom devices) and `flutter doctor` after install. | ```sh -emb flutter --flutter-version 3.44.2 --configure +emb flutter -w /tmp/ws1 --flutter-version 3.44.2 +. /tmp/ws1/setup_env.sh # FLUTTER_WORKSPACE=/tmp/ws1 ``` --- @@ -462,17 +465,25 @@ emb cross . --target rpi5 --build ### `emb env` -Write `setup_env.sh` (`PATH` for Flutter/Dart, `FLUTTER_WORKSPACE`, `PUB_CACHE`, -`XDG_CONFIG_HOME`, the engine version, …). +(Re)generate or print `setup_env.sh` (`PATH` for Flutter/Dart, +`FLUTTER_WORKSPACE`, `PUB_CACHE`, `XDG_CONFIG_HOME`, the engine version, …). + +`emb setup` and `emb flutter` already emit this file, so the standalone command +is for what they don't cover: regenerating after the workspace has **moved** +(the baked-in `FLUTTER_WORKSPACE` is absolute), previewing the env on stdout, or +writing it to a custom path. It has no side effects beyond the one file — it +never clones or installs, and warns if `/flutter` is absent (the env +would point at a missing SDK; run `emb flutter -w ` first). | Option | Default | Description | |---|---|---| -| `-w`, `--workspace ` | resolution order | Workspace root. | +| `-w`, `--workspace ` | resolution order | Workspace root (becomes `FLUTTER_WORKSPACE`). | | `-o`, `--output ` | `/setup_env.sh` | Output file path. | | `--print` | off | Print to stdout instead of writing a file. | ```sh -emb env # write /setup_env.sh +emb env # (re)write /setup_env.sh +emb env -w /tmp/ws1 # regenerate after moving the workspace emb env --print # preview on stdout ``` diff --git a/lib/src/commands/env_command.dart b/lib/src/commands/env_command.dart index 1b6a557..cc8069b 100644 --- a/lib/src/commands/env_command.dart +++ b/lib/src/commands/env_command.dart @@ -8,8 +8,15 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; /// {@template env_command} -/// `emb env` — write `setup_env.sh` for the workspace (Flutter/Dart on PATH, -/// FLUTTER_WORKSPACE, PUB_CACHE, …). Source it with `. ./setup_env.sh`. +/// `emb env` — (re)generate or print `setup_env.sh` for a workspace +/// (Flutter/Dart on PATH, FLUTTER_WORKSPACE, PUB_CACHE, …). Source it with +/// `. ./setup_env.sh`. +/// +/// `emb setup` and `emb flutter` already emit this file as part of their work, +/// so the standalone command is for the cases they don't cover: regenerating +/// after the workspace has moved (the baked-in FLUTTER_WORKSPACE is absolute), +/// printing the env to stdout (`--print`), or writing it to a custom path. It +/// has no side effects beyond the one file — it never clones or installs. /// {@endtemplate} class EnvCommand extends Command { /// {@macro env_command} @@ -39,7 +46,7 @@ class EnvCommand extends Command { @override String get description => - 'Write setup_env.sh (PATH, FLUTTER_WORKSPACE, PUB_CACHE, …).'; + '(Re)generate or print setup_env.sh (PATH, FLUTTER_WORKSPACE, …).'; @override String get name => 'env'; @@ -50,6 +57,16 @@ class EnvCommand extends Command { final host = _host ?? HostInfo.detect(); final workspace = Workspace.resolve(override: args['workspace'] as String?); + // The env points PATH at /flutter; warn if no SDK is there yet + // (e.g. `emb env` run in a bare dir), since the script will reference a + // not-yet-present SDK. Install it with `emb flutter -w `. + if (!workspace.flutterDir.existsSync()) { + _logger.warn( + 'No Flutter SDK at ${workspace.flutterDir.path} — the env will point ' + 'at a missing SDK. Run: emb flutter -w ${workspace.root.path}', + ); + } + final script = generateSetupEnv( workspace: workspace, host: host, @@ -64,7 +81,11 @@ class EnvCommand extends Command { final out = (args['output'] as String?) ?? p.join(workspace.root.path, 'setup_env.sh'); - File(out).writeAsStringSync(script); + // Create the target dir first: `emb env -w ` is a normal way to + // seed env for a workspace that doesn't exist on disk yet. + File(out) + ..parent.createSync(recursive: true) + ..writeAsStringSync(script); _logger ..info('Wrote $out') ..info('Source it: . ${p.relative(out)}'); diff --git a/lib/src/commands/flutter_command.dart b/lib/src/commands/flutter_command.dart index 32f04c1..efd531f 100644 --- a/lib/src/commands/flutter_command.dart +++ b/lib/src/commands/flutter_command.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:emb_cli/src/env/env_script.dart'; import 'package:emb_cli/src/flutter/flutter_sdk.dart'; import 'package:emb_cli/src/host/host_info.dart'; import 'package:emb_cli/src/workspace/workspace.dart'; @@ -103,6 +104,19 @@ class FlutterCommand extends Command { return ExitCode.software.code; } } + + // Emit setup_env.sh into the resolved workspace so FLUTTER_WORKSPACE (and + // the SDK's Flutter/Dart on PATH) match where the SDK was just installed — + // i.e. the -w target, not the cwd a later `emb env` would default to. + final envFile = File(p.join(workspace.root.path, 'setup_env.sh')) + ..writeAsStringSync( + generateSetupEnv( + workspace: workspace, + host: host, + engineVersion: result.engineCommit, + ), + ); + _logger.info('Source the env: . ${p.relative(envFile.path)}'); return ExitCode.success.code; } diff --git a/test/src/commands/env_command_test.dart b/test/src/commands/env_command_test.dart new file mode 100644 index 0000000..8ba874a --- /dev/null +++ b/test/src/commands/env_command_test.dart @@ -0,0 +1,50 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:emb_cli/src/commands/env_command.dart'; +import 'package:emb_cli/src/host/host_info.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +const _host = HostInfo( + os: HostOs.linux, + machineArch: 'x86_64', + archAliases: {'x86_64', 'x64', 'amd64'}, + hostType: 'fedora', + versionId: '43', +); + +void main() { + late Directory tmp; + setUp(() => tmp = Directory.systemTemp.createTempSync('emb_envcmd_')); + tearDown(() => tmp.deleteSync(recursive: true)); + + Future run(List args) { + final runner = CommandRunner('emb', 'test') + ..addCommand(EnvCommand(logger: Logger(), host: _host)); + return runner.run(args); + } + + test('writes setup_env.sh with FLUTTER_WORKSPACE from -w', () async { + final ws = p.join(tmp.path, 'ws1'); + final code = await run(['env', '-w', ws]); + expect(code, ExitCode.success.code); + + final text = File(p.join(ws, 'setup_env.sh')).readAsStringSync(); + expect(text, contains('export FLUTTER_WORKSPACE="$ws"')); + }); + + test('succeeds (warns, does not fail) when the SDK is absent', () async { + // /flutter does not exist — the command warns but still writes. + final code = await run(['env', '-w', tmp.path]); + expect(code, ExitCode.success.code); + expect(File(p.join(tmp.path, 'setup_env.sh')).existsSync(), isTrue); + }); + + test('--print does not write a file', () async { + final code = await run(['env', '-w', tmp.path, '--print']); + expect(code, ExitCode.success.code); + expect(File(p.join(tmp.path, 'setup_env.sh')).existsSync(), isFalse); + }); +} diff --git a/test/src/commands/flutter_command_test.dart b/test/src/commands/flutter_command_test.dart new file mode 100644 index 0000000..eb34509 --- /dev/null +++ b/test/src/commands/flutter_command_test.dart @@ -0,0 +1,75 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:emb_cli/src/commands/flutter_command.dart'; +import 'package:emb_cli/src/flutter/flutter_sdk.dart'; +import 'package:emb_cli/src/host/host_info.dart'; +import 'package:emb_cli/src/repo/git_repo.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +const _host = HostInfo( + os: HostOs.linux, + machineArch: 'x86_64', + archAliases: {'x86_64', 'x64', 'amd64'}, + hostType: 'fedora', + versionId: '43', +); + +/// Simulates a successful Flutter SDK install without touching git: creates the +/// SDK dir (and thus the workspace root) and reports an engine commit. +class _FakeSdk extends FlutterSdk { + _FakeSdk(super.workspace); + + @override + Future install( + String version, { + GitRunner runner = defaultGitRunner, + }) async { + workspace.flutterDir.createSync(recursive: true); + return FlutterInstallResult( + success: true, + path: workspace.flutterDir.path, + engineCommit: 'deadbeefcafe', + ); + } +} + +void main() { + late Directory tmp; + setUp(() => tmp = Directory.systemTemp.createTempSync('emb_flcmd_')); + tearDown(() => tmp.deleteSync(recursive: true)); + + Future run(List args) { + final runner = CommandRunner('emb', 'test') + ..addCommand( + FlutterCommand( + logger: Logger(), + host: _host, + sdkFactory: (ws, host) => _FakeSdk(ws), + ), + ); + return runner.run(args); + } + + test('writes setup_env.sh with FLUTTER_WORKSPACE set to -w', () async { + final ws = p.join(tmp.path, 'ws1'); + final code = await run([ + 'flutter', + '-w', + ws, + '--flutter-version', + '3.44.2', + ]); + expect(code, ExitCode.success.code); + + final env = File(p.join(ws, 'setup_env.sh')); + expect(env.existsSync(), isTrue); + final text = env.readAsStringSync(); + // The -w target is the workspace, not the cwd a later bare `emb env` uses. + expect(text, contains('export FLUTTER_WORKSPACE="$ws"')); + // The engine commit read during install is carried into the env script. + expect(text, contains('FLUTTER_ENGINE_VERSION="deadbeefcafe"')); + }); +}