scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
204 lines
6.7 KiB
Dart
204 lines
6.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as path;
|
|
|
|
typedef SymbolUploader =
|
|
Future<int> Function({
|
|
required List<String> arguments,
|
|
required Map<String, String> environment,
|
|
required String workingDirectory,
|
|
});
|
|
|
|
const _defaultSentryUrl = 'https://bugs.plezy.app';
|
|
|
|
Future<void> main(List<String> arguments) async {
|
|
final scriptDirectory = File.fromUri(Platform.script).parent;
|
|
final result = await runUploadSymbols(arguments, repositoryRoot: scriptDirectory.parent);
|
|
if (result != 0) {
|
|
exitCode = result;
|
|
}
|
|
}
|
|
|
|
Future<int> runUploadSymbols(
|
|
List<String> arguments, {
|
|
required Directory repositoryRoot,
|
|
Map<String, String>? environment,
|
|
SymbolUploader uploader = _runSentryPlugin,
|
|
Future<String?> Function()? revisionProvider,
|
|
StringSink? output,
|
|
StringSink? errors,
|
|
}) async {
|
|
final out = output ?? stdout;
|
|
final err = errors ?? stderr;
|
|
if (arguments.isEmpty || arguments.first.isEmpty) {
|
|
err.writeln('platform arg required');
|
|
return 1;
|
|
}
|
|
|
|
final platform = arguments.first;
|
|
final root = path.normalize(path.absolute(repositoryRoot.path));
|
|
final sourceArgument = arguments.length > 1 ? arguments[1] : '';
|
|
final sourceRoot = sourceArgument.isEmpty
|
|
? root
|
|
: path.normalize(path.isAbsolute(sourceArgument) ? sourceArgument : path.join(root, sourceArgument));
|
|
final buildRoot = path.join(sourceRoot, 'build');
|
|
final symbolRoot = path.join(sourceRoot, 'debug-info', platform);
|
|
|
|
final searchRoots = <String>[];
|
|
void addExistingRoot(String candidate) {
|
|
if (Directory(candidate).existsSync()) {
|
|
searchRoots.add(candidate);
|
|
}
|
|
}
|
|
|
|
addExistingRoot(symbolRoot);
|
|
switch (platform) {
|
|
case 'macos':
|
|
addExistingRoot(path.join(buildRoot, 'macos'));
|
|
case 'ios':
|
|
addExistingRoot(path.join(buildRoot, 'ios'));
|
|
addExistingRoot(path.join(sourceRoot, 'ios', 'build'));
|
|
case 'linux-x64' || 'linux-arm64':
|
|
addExistingRoot(path.join(buildRoot, 'linux'));
|
|
case 'android-apk' || 'android-aab':
|
|
addExistingRoot(path.join(buildRoot, 'app'));
|
|
case 'windows-x64' || 'windows-arm64':
|
|
addExistingRoot(path.join(buildRoot, 'windows'));
|
|
default:
|
|
err.writeln('unknown platform: $platform');
|
|
return 2;
|
|
}
|
|
|
|
final symbolFiles = _discoverFiles(searchRoots);
|
|
if (symbolFiles.isEmpty) {
|
|
err.writeln('no symbols found for platform $platform');
|
|
return 3;
|
|
}
|
|
|
|
final childEnvironment = Map<String, String>.of(environment ?? Platform.environment);
|
|
final dryRun = _isNotEmpty(childEnvironment['BUGS_UPLOAD_DRY_RUN']);
|
|
childEnvironment['SENTRY_URL'] = _firstNotEmpty([
|
|
childEnvironment['SENTRY_URL'],
|
|
childEnvironment['BUGS_URL'],
|
|
_defaultSentryUrl,
|
|
]);
|
|
childEnvironment['SENTRY_LOG_LEVEL'] = _firstNotEmpty([childEnvironment['SENTRY_LOG_LEVEL'], 'info']);
|
|
if (!_isNotEmpty(childEnvironment['SENTRY_AUTH_TOKEN']) && _isNotEmpty(childEnvironment['BUGS_ADMIN_TOKEN'])) {
|
|
childEnvironment['SENTRY_AUTH_TOKEN'] = childEnvironment['BUGS_ADMIN_TOKEN']!;
|
|
}
|
|
if (!dryRun && !_isNotEmpty(childEnvironment['SENTRY_AUTH_TOKEN'])) {
|
|
err.writeln('SENTRY_AUTH_TOKEN or BUGS_ADMIN_TOKEN env var required');
|
|
return 1;
|
|
}
|
|
|
|
if (!_isNotEmpty(childEnvironment['SENTRY_RELEASE'])) {
|
|
final revision = await (revisionProvider?.call() ?? _gitRevision(root));
|
|
if (!_isNotEmpty(revision)) {
|
|
err.writeln('failed to determine SENTRY_RELEASE from git');
|
|
return 1;
|
|
}
|
|
childEnvironment['SENTRY_RELEASE'] = 'plezy@$revision';
|
|
}
|
|
|
|
var dartSymbolMapPath = childEnvironment['SENTRY_DART_SYMBOL_MAP_PATH'] ?? '';
|
|
if (dartSymbolMapPath.isEmpty) {
|
|
for (final candidate in [
|
|
path.join(symbolRoot, 'obfuscation.map.json'),
|
|
path.join(buildRoot, 'app', 'obfuscation', '$platform.map.json'),
|
|
path.join(buildRoot, 'app', 'obfuscation.map.json'),
|
|
]) {
|
|
if (File(candidate).existsSync()) {
|
|
dartSymbolMapPath = candidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
final release = childEnvironment['SENTRY_RELEASE']!;
|
|
final dist = childEnvironment['SENTRY_DIST'] ?? '';
|
|
final pluginArguments = <String>[
|
|
'--sentry-define=release=$release',
|
|
'--sentry-define=url=${childEnvironment['SENTRY_URL']}',
|
|
'--sentry-define=build_path=$buildRoot',
|
|
if (dist.isNotEmpty) '--sentry-define=dist=$dist',
|
|
if (Directory(symbolRoot).existsSync()) '--sentry-define=symbols_path=$symbolRoot',
|
|
if (dartSymbolMapPath.isNotEmpty) '--sentry-define=dart_symbol_map_path=$dartSymbolMapPath',
|
|
];
|
|
|
|
if (dryRun) {
|
|
out.writeln('dry-run: would upload symbols for $platform');
|
|
out.writeln('dry-run: release=$release');
|
|
out.writeln('dry-run: dist=$dist');
|
|
out.writeln('dry-run: source_root=$sourceRoot');
|
|
out.writeln('dry-run: build_path=$buildRoot');
|
|
out.writeln('dry-run: symbols_path=$symbolRoot');
|
|
out.writeln('dry-run: dart_symbol_map_path=$dartSymbolMapPath');
|
|
for (final file in symbolFiles) {
|
|
out.writeln(file);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
out.writeln('uploading symbols for $platform release $release dist $dist');
|
|
try {
|
|
final result = await uploader(arguments: pluginArguments, environment: childEnvironment, workingDirectory: root);
|
|
if (result != 0) {
|
|
err.writeln('symbol upload failed for $platform (exit code $result)');
|
|
}
|
|
return result;
|
|
} on ProcessException catch (error) {
|
|
err.writeln('failed to start symbol upload: $error');
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
List<String> _discoverFiles(List<String> roots) {
|
|
final files = <String>[];
|
|
for (final root in roots) {
|
|
try {
|
|
files.addAll(
|
|
Directory(root)
|
|
.listSync(recursive: true, followLinks: false)
|
|
.whereType<File>()
|
|
.map((file) => path.normalize(path.absolute(file.path))),
|
|
);
|
|
} on FileSystemException {
|
|
// Match the wrappers' previous best-effort discovery behavior.
|
|
}
|
|
}
|
|
files.sort();
|
|
return files;
|
|
}
|
|
|
|
Future<String?> _gitRevision(String root) async {
|
|
try {
|
|
final result = await Process.run('git', const ['rev-parse', '--short', 'HEAD'], workingDirectory: root);
|
|
if (result.exitCode == 0) {
|
|
return (result.stdout as String).trim();
|
|
}
|
|
} on ProcessException {
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<int> _runSentryPlugin({
|
|
required List<String> arguments,
|
|
required Map<String, String> environment,
|
|
required String workingDirectory,
|
|
}) async {
|
|
final process = await Process.start(
|
|
Platform.resolvedExecutable,
|
|
['run', 'sentry_dart_plugin', ...arguments],
|
|
workingDirectory: workingDirectory,
|
|
environment: environment,
|
|
includeParentEnvironment: false,
|
|
mode: ProcessStartMode.inheritStdio,
|
|
);
|
|
return process.exitCode;
|
|
}
|
|
|
|
bool _isNotEmpty(String? value) => value != null && value.isNotEmpty;
|
|
|
|
String _firstNotEmpty(List<String?> values) => values.firstWhere(_isNotEmpty)!;
|