Merge pull request #30 from toyota-connected/feat/flutter-env-setup

fix(env): make setup_env.sh follow the -w workspace
This commit is contained in:
Joel Winarske
2026-06-18 08:34:08 -07:00
committed by GitHub
5 changed files with 182 additions and 11 deletions
+18 -7
View File
@@ -215,17 +215,20 @@ emb sync --config ../configs -j 8
### `emb flutter`
Install the Flutter SDK into `<workspace>/flutter`.
Install the Flutter SDK into `<workspace>/flutter`, then emit
`<workspace>/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 <dir>` | resolution order | Workspace root. |
| `-w`, `--workspace <dir>` | resolution order | Workspace root. Also becomes `FLUTTER_WORKSPACE` in the emitted `setup_env.sh`. |
| `--flutter-version <ref>` | `globals.json` `flutter_version` | Version/tag/branch to check out. |
| `-c`, `--config <dir>` | `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 `<workspace>/flutter` is absent (the env
would point at a missing SDK; run `emb flutter -w <root>` first).
| Option | Default | Description |
|---|---|---|
| `-w`, `--workspace <dir>` | resolution order | Workspace root. |
| `-w`, `--workspace <dir>` | resolution order | Workspace root (becomes `FLUTTER_WORKSPACE`). |
| `-o`, `--output <path>` | `<workspace>/setup_env.sh` | Output file path. |
| `--print` | off | Print to stdout instead of writing a file. |
```sh
emb env # write <workspace>/setup_env.sh
emb env # (re)write <workspace>/setup_env.sh
emb env -w /tmp/ws1 # regenerate after moving the workspace
emb env --print # preview on stdout
```
+25 -4
View File
@@ -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<int> {
/// {@macro env_command}
@@ -39,7 +46,7 @@ class EnvCommand extends Command<int> {
@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<int> {
final host = _host ?? HostInfo.detect();
final workspace = Workspace.resolve(override: args['workspace'] as String?);
// The env points PATH at <workspace>/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 <root>`.
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<int> {
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 <new path>` 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)}');
+14
View File
@@ -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<int> {
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;
}
+50
View File
@@ -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<int?> run(List<String> args) {
final runner = CommandRunner<int>('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 {
// <workspace>/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);
});
}
@@ -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<FlutterInstallResult> 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<int?> run(List<String> args) {
final runner = CommandRunner<int>('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"'));
});
}