-
Notifications
You must be signed in to change notification settings - Fork 5
/
sidekick_core.dart
385 lines (336 loc) · 12.9 KB
/
sidekick_core.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
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
194
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
library sidekick_core;
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:dartx/dartx_io.dart';
import 'package:dcli/dcli.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:sidekick_core/src/commands/update_command.dart';
import 'package:sidekick_core/src/dart_package.dart';
import 'package:sidekick_core/src/repository.dart';
import 'package:sidekick_core/src/version_checker.dart';
export 'dart:io' hide sleep;
export 'package:args/command_runner.dart';
export 'package:dartx/dartx.dart';
export 'package:dartx/dartx_io.dart';
export 'package:dcli/dcli.dart' hide run, start, startFromArgs, absolute;
export 'package:pub_semver/pub_semver.dart' show Version;
export 'package:sidekick_core/src/cli_util.dart';
export 'package:sidekick_core/src/commands/analyze_command.dart';
export 'package:sidekick_core/src/commands/bash_command.dart';
export 'package:sidekick_core/src/commands/dart_command.dart';
export 'package:sidekick_core/src/commands/deps_command.dart';
export 'package:sidekick_core/src/commands/flutter_command.dart';
export 'package:sidekick_core/src/commands/install_global_command.dart';
export 'package:sidekick_core/src/commands/plugins/plugins_command.dart';
export 'package:sidekick_core/src/commands/recompile_command.dart';
export 'package:sidekick_core/src/commands/sidekick_command.dart';
export 'package:sidekick_core/src/dart.dart';
export 'package:sidekick_core/src/dart_package.dart';
export 'package:sidekick_core/src/dart_runtime.dart';
export 'package:sidekick_core/src/file_util.dart';
export 'package:sidekick_core/src/flutter.dart';
export 'package:sidekick_core/src/flutterw.dart';
export 'package:sidekick_core/src/forward_command.dart';
export 'package:sidekick_core/src/git.dart';
export 'package:sidekick_core/src/repository.dart';
export 'package:sidekick_core/src/sidekick_package.dart';
export 'package:sidekick_core/src/template/sidekick_package.template.dart';
/// The version of package:sidekick_core
///
/// This is used by the update command to determine if your sidekick cli
/// requires an update
// DO NOT MANUALLY EDIT THIS VERSION, instead run `sk bump-version sidekick_core`
final Version version = Version.parse('0.14.0');
/// Initializes sidekick, call this at the very start of your CLI program
///
/// Set [name] to the name of your CLI entrypoint
///
/// [mainProjectPath] should be set when you have a package that you
/// consider the main package of the whole repository.
/// When your repository contains only one Flutter package in root set
/// `mainProjectPath = '.'`.
/// In a multi package repository you might use the same when the main package
/// is in root, or `mainProjectPath = 'packages/my_app'` when it is in a subfolder.
///
/// Set [flutterSdkPath] when you bind a flutter sdk to this project. This SDK
/// enables the [flutter] and [dart] commands.
/// [dartSdkPath] is inherited from [flutterSdkPath]. Set it only for pure dart
/// projects.
/// The paths can either be absolute or relative to the project root. (E.g. if
/// the custom sidekick CLI is at /Users/foo/project-x/packages/custom_sidekick,
/// relative paths are resolved relative to /Users/foo/project-x)
SidekickCommandRunner initializeSidekick({
required String name,
String? description,
String? mainProjectPath,
String? flutterSdkPath,
String? dartSdkPath,
}) {
DartPackage? mainProject;
final repo = findRepository();
if (mainProjectPath != null) {
mainProject =
DartPackage.fromDirectory(repo.root.directory(mainProjectPath));
}
if (flutterSdkPath != null && dartSdkPath != null) {
printerr("It's unnecessary to set both `flutterSdkPath` and `dartSdkPath`, "
"because `dartSdkPath` is inherited from `flutterSdkPath. "
"Set `dartSdkPath` only for pure dart projects.");
}
final runner = SidekickCommandRunner._(
cliName: name,
description: description ??
'A sidekick CLI to equip Dart/Flutter projects with custom tasks',
repository: repo,
mainProject: mainProject,
workingDirectory: Directory.current,
flutterSdk: _resolveSdkPath(flutterSdkPath, repo.root),
dartSdk: _resolveSdkPath(dartSdkPath, repo.root),
);
return runner;
}
/// A CommandRunner that mounts the sidekick globals
/// [entryWorkingDirectory], [cliName], [repository], [mainProject].
class SidekickCommandRunner<T> extends CommandRunner<T> {
SidekickCommandRunner._({
required String cliName,
required String description,
required this.repository,
this.mainProject,
required this.workingDirectory,
this.flutterSdk,
this.dartSdk,
}) : super(cliName, description) {
argParser.addFlag(
'version',
negatable: false,
help: 'Print the sidekick version of this CLI.',
);
}
final Repository repository;
final DartPackage? mainProject;
final Directory workingDirectory;
final Directory? flutterSdk;
final Directory? dartSdk;
/// Mounts the sidekick related globals, returns a function to unmount them
/// and restore the previous globals
Unmount mount() {
final SidekickCommandRunner? oldRunner = _activeRunner;
_activeRunner = this;
_entryWorkingDirectory = workingDirectory;
return () {
_activeRunner = oldRunner;
_entryWorkingDirectory = _activeRunner?.workingDirectory;
};
}
@override
Future<T?> run(Iterable<String> args) async {
// a new command gets executes, reset whatever exitCode the previous command has set
exitCode = 0;
final unmount = mount();
ArgResults? parsedArgs;
try {
parsedArgs = parse(args);
if (parsedArgs['version'] == true) {
print('$cliName is using sidekick version $version');
return null;
}
final result = await super.runCommand(parsedArgs);
return result;
} finally {
if (_isUpdateCheckEnabled && !_isSidekickCliUpdateCommand(parsedArgs)) {
// print warning if the user didn't fully update their CLI
_checkCliVersionIntegrity();
// print warning if CLI update is available
// TODO start the update check in the background at command start
// TODO prevent multiple update checks when a command start another command
await _checkForUpdates();
}
unmount();
}
}
/// Print a warning if the CLI isn't up to date
Future<void> _checkForUpdates() async {
try {
final updateFuture = VersionChecker.isDependencyUpToDate(
package: Repository.requiredSidekickPackage,
dependency: 'sidekick_core',
pubspecKeys: ['sidekick', 'cli_version'],
);
// If it takes too long, don't wait for it
final isUpToDate = await updateFuture.timeout(const Duration(seconds: 3));
if (!isUpToDate) {
printerr(
'${yellow('Update available!')}\n'
'Run ${cyan('$cliName sidekick update')} to update your CLI.',
);
}
} catch (_) {
/* ignore */
}
}
/// Print a warning if the user manually updated the sidekick_core
/// minimum version of their CLI and that version doesn't match with the
/// CLI version listed in the pubspec at the path ['sidekick', 'cli_version']
void _checkCliVersionIntegrity() {
final sidekickCoreVersion = VersionChecker.getMinimumVersionConstraint(
Repository.requiredSidekickPackage,
['dependencies', 'sidekick_core'],
);
final sidekickCliVersion = VersionChecker.getMinimumVersionConstraint(
Repository.requiredSidekickPackage,
['sidekick', 'cli_version'],
);
// old CLI which has no version information yet
// _checkForUpdates will print a warning to update the CLI in this case
if (sidekickCliVersion == Version.none) {
return;
}
if (sidekickCliVersion != sidekickCoreVersion) {
printerr(
'The sidekick_core version is incompatible with the bash scripts '
'in /tool and entrypoint because you probably updated the '
'sidekick_core dependency of your CLI package manually.\n'
'Please run ${cyan('$cliName sidekick update')} to repair your CLI.',
);
}
}
/// Returns true if the command executed from [parsedArgs] is [UpdateCommand]
///
/// Copied and adapted from CommandRunner.runCommand
bool _isSidekickCliUpdateCommand(ArgResults? parsedArgs) {
if (parsedArgs == null) {
return false;
}
var argResults = parsedArgs;
Command? command;
var commands = Map.of(this.commands);
while (commands.isNotEmpty) {
if (argResults.command == null) {
return false;
}
// Step into the command.
argResults = argResults.command!;
command = commands[argResults.name];
commands = Map.from(command!.subcommands);
}
if (parsedArgs['help'] as bool) {
// execute HelpCommand from args library
return false;
}
return command is UpdateCommand;
}
}
/// Enables the [SidekickCommandRunner] to check for `sidekick` updates
bool get _isUpdateCheckEnabled => env['SIDEKICK_ENABLE_UPDATE_CHECK'] == 'true';
typedef Unmount = void Function();
@Deprecated('noop')
void deinitializeSidekick() {}
/// The runner that is currently executing, used for nesting
SidekickCommandRunner? _activeRunner;
/// The working directory (cwd) from which the cli was started
Directory get entryWorkingDirectory =>
_entryWorkingDirectory ?? Directory.current;
Directory? _entryWorkingDirectory;
/// Name of the cli program
///
/// Usually a short acronym, like 3 characters
String get cliName {
if (_activeRunner == null) {
throw OutOfCommandRunnerScopeException('cliName');
}
return _activeRunner!.executableName;
}
/// Name of the cli program (if running a generated sidekick CLI)
/// or null (if running the global sidekick CLI)
String? get cliNameOrNull => _activeRunner?.executableName;
/// The root of the repository which contains all projects
Repository get repository {
if (_activeRunner == null) {
throw OutOfCommandRunnerScopeException('repository');
}
return _activeRunner!.repository;
}
/// The main package which should be executed by default
///
/// The mainProjectPath has to be set by the user in [initializeSidekick].
/// It's optional, not every project has a mainProject, there are repositories
/// with zero or multiple projects.
DartPackage? get mainProject {
if (_activeRunner == null) {
throw OutOfCommandRunnerScopeException('mainProject');
}
return _activeRunner?.mainProject;
}
/// Returns the path to he Flutter SDK sidekick should use for the [flutter] command
///
/// This variable is usually set to a pinned version of the Flutter SDK per project, i.e.
/// - https://github.com/passsy/flutter_wrapper
/// - https://github.com/fluttertools/fvm
Directory? get flutterSdk {
if (_activeRunner == null) {
throw OutOfCommandRunnerScopeException('flutterSdk');
}
return _activeRunner?.flutterSdk;
}
/// Returns the path to the Dart SDK sidekick should use for the [dart] command
///
/// Usually inherited from [flutterSdk] which ships with an embedded Dart SDK
Directory? get dartSdk {
if (_activeRunner == null) {
throw OutOfCommandRunnerScopeException('dartSdk');
}
return _activeRunner?.dartSdk;
}
/// The Dart or Flutter SDK path is set in [initializeSidekick],
/// but the directory doesn't exist
class SdkNotFoundException implements Exception {
SdkNotFoundException(this.sdkPath, this.repoRoot);
final String sdkPath;
final Directory repoRoot;
late final String message =
"Dart or Flutter SDK set to '$sdkPath', but that directory doesn't exist. "
"Please fix the path in `initializeSidekick` (dartSdkPath/flutterSdkPath). "
"Note that relative sdk paths are resolved relative to the project root, "
"which in this case is '${repoRoot.path}'.";
@override
String toString() {
return "SdkNotFoundException{message: $message}";
}
}
/// Transforms [sdkPath] to an absolute directory
///
/// This is to make passing `flutterSdkPath`/`dartSdkPath`
/// in `initializeSidekick` a relative path work from anywhere.
///
/// If [sdkPath] is a relative path, it is resolved relative to
/// the project root [repoRoot].
///
/// Throws a [SdkNotFoundException] if [sdkPath] is given but no
/// existing directory can be found.
Directory? _resolveSdkPath(String? sdkPath, Directory repoRoot) {
if (sdkPath == null) {
return null;
}
final resolvedDir = (Directory(sdkPath).isAbsolute
? Directory(sdkPath)
// resolve relative path relative to project root
: repoRoot.directory(sdkPath))
.absolute;
if (!resolvedDir.existsSync()) {
throw SdkNotFoundException(sdkPath, repoRoot);
}
return resolvedDir;
}
/// Called when properties of [SidekickCommandRunner] are accessed outside of
/// the execution of a command
class OutOfCommandRunnerScopeException implements Exception {
String get message => "Can't access SidekickCommandRunner.$property "
"when no command is executed.";
final String property;
OutOfCommandRunnerScopeException(this.property);
@override
String toString() {
return "OutOfCommandRunnerScopeException{message: $message}";
}
}