import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:path/path.dart' as p; import 'package:yaml/yaml.dart' as yaml; const _lockFileName = 'codegen-lock.yaml'; void main() async { final root = _projectRoot(); var ran = false; if (_iconsOutOfDate(root)) { final inputs = _iconsInputs(root); stdout.writeln('Icons out of date, running flutter_launcher_icons...'); await _run('dart', ['run', 'flutter_launcher_icons'], root); _updateLockWithHashes(root, 'icons', _computeHashes(root, inputs)); ran = true; } if (_l10nOutOfDate(root)) { final inputs = _l10nInputs(root); stdout.writeln('l10n out of date, running flutter gen-l10n...'); await _run('flutter', [ 'gen-l10n', '--template-arb-file', 'app_hu.arb', ], root); _updateLockWithHashes(root, 'l10n', _computeHashes(root, inputs)); ran = true; } if (_isarOutOfDate(root) || _isarGeneratedFilesMissing(root)) { final inputs = _isarInputs(root); final hashes = _computeHashes(root, inputs); stdout.writeln( 'Isar generated dart files out of date or missing, running build_runner...', ); await _run('dart', ['run', 'build_runner', 'build'], root); _updateLockWithHashes(root, 'isar', hashes); ran = true; } if (!ran) { stdout.writeln('All generated files are up to date.'); } } String _projectRoot() { final script = p.canonicalize(Platform.script.toFilePath()); return p.canonicalize(p.dirname(p.dirname(script))); } String _lockPath(String root) => p.join(root, _lockFileName); Map>? _readLock(String root) { final file = File(_lockPath(root)); if (!file.existsSync()) return null; try { final content = file.readAsStringSync(); final decoded = yaml.loadYaml(content); if (decoded is! Map) return null; final result = >{}; for (final entry in decoded.entries) { if (entry.value is! Map) continue; final inner = entry.value as Map; result[entry.key.toString()] = inner.map( (k, v) => MapEntry( Platform.isWindows ? k.toString().toLowerCase() : k.toString(), v?.toString() ?? '', ), ); } return result; } catch (_) { return null; } } void _writeLock(String root, Map> lock) { final buf = StringBuffer(); for (final stepEntry in lock.entries) { buf.writeln('${stepEntry.key}:'); for (final fileEntry in stepEntry.value.entries) { buf.writeln( ' "${_escapeYaml(fileEntry.key)}": "${_escapeYaml(fileEntry.value)}"', ); } } File(_lockPath(root)).writeAsStringSync(buf.toString()); } String _escapeYaml(String s) => s.replaceAll('\\', '\\\\').replaceAll('"', '\\"'); String _fileHash(File file) { final bytes = file.readAsBytesSync(); final digest = sha256.convert(bytes); return digest.toString(); } String _relativePath(String root, File file) { final rel = p.relative(file.path, from: root); final normalized = rel.replaceAll('\\', '/'); return Platform.isWindows ? normalized.toLowerCase() : normalized; } bool _hashesMatch( String root, String stepName, List inputs, Map>? lock, ) { if (lock == null || !lock.containsKey(stepName)) return false; final stepHashes = lock[stepName]!; for (final f in inputs) { final rel = _relativePath(root, f); final stored = stepHashes[rel]; if (stored == null || stored != _fileHash(f)) return false; } if (stepHashes.length != inputs.length) return false; return true; } Map _computeHashes(String root, List inputs) { return {for (final f in inputs) _relativePath(root, f): _fileHash(f)}; } void _updateLockWithHashes( String root, String stepName, Map hashes, ) { final lock = _readLock(root) ?? >{}; lock[stepName] = Map.from(hashes); _writeLock(root, lock); } DateTime? _modified(File file) { if (!file.existsSync()) return null; return file.lastModifiedSync(); } bool _anyNewerThan(Iterable inputs, File output) { final outTime = _modified(output); if (outTime == null) return true; for (final f in inputs) { final t = _modified(f); if (t != null && t.isAfter(outTime)) return true; } return false; } List _iconsInputs(String root) { final config = File(p.join(root, 'flutter_launcher_icons.yaml')); final pubspec = File(p.join(root, 'pubspec.yaml')); final imagePath = File(p.join(root, 'assets/images/logos/colored_logo.png')); final monochrome = File( p.join(root, 'assets/images/logos/monochrome_logo.png'), ); final background = File( p.join(root, 'assets/images/logos/colored_logo_without_mustache.png'), ); final foreground = File( p.join(root, 'assets/images/logos/colored_logo_only_mustache.png'), ); return [config, pubspec, imagePath, monochrome, background, foreground] .where((f) => f.existsSync()) .map((f) => File(p.canonicalize(f.path))) .toList(); } bool _iconsOutOfDate(String root) { final inputs = _iconsInputs(root); final output = File( p.join( root, 'android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml', ), ); if (!_anyNewerThan(inputs, output)) return false; return !_hashesMatch(root, 'icons', inputs, _readLock(root)); } List _l10nInputs(String root) { final l10nDir = p.join(root, 'lib/l10n'); final l10nYml = File(p.join(root, 'l10n.yml')); final arbs = Directory(l10nDir) .listSync() .whereType() .where((f) => f.path.endsWith('.arb')) .map((f) => File(p.canonicalize(f.path))) .toList(); return [l10nYml, ...arbs].where((f) => f.existsSync()).cast().toList(); } bool _l10nOutOfDate(String root) { final inputs = _l10nInputs(root); final output = File(p.join(root, 'lib/l10n/app_localizations.dart')); if (!_anyNewerThan(inputs, output)) return false; return !_hashesMatch(root, 'l10n', inputs, _readLock(root)); } List _isarInputs(String root) { final modelsDir = p.join(root, 'lib/data/models'); if (!Directory(modelsDir).existsSync()) return []; final list = []; for (final entity in Directory(modelsDir).listSync()) { if (entity is! File || !entity.path.endsWith('.dart')) continue; final content = entity.readAsStringSync(); if (!content.contains("part '") || !content.contains('.g.dart')) continue; list.add(File(p.canonicalize(entity.path))); } return list; } bool _isarOutOfDate(String root) { final inputs = _isarInputs(root); if (inputs.isEmpty) return false; final modelsDir = p.join(root, 'lib/data/models'); for (final dartFile in inputs) { final baseName = p.basenameWithoutExtension(dartFile.path); final gFile = File(p.join(modelsDir, '$baseName.g.dart')); if (_anyNewerThan([dartFile], gFile)) { return !_hashesMatch(root, 'isar', inputs, _readLock(root)); } } return false; } bool _isarGeneratedFilesMissing(String root) { final inputs = _isarInputs(root); if (inputs.isEmpty) return false; final modelsDir = p.join(root, 'lib/data/models'); for (final dartFile in inputs) { final baseName = p.basenameWithoutExtension(dartFile.path); final gFile = File(p.join(modelsDir, '$baseName.g.dart')); if (!gFile.existsSync()) return true; } return false; } Future _run( String executable, List args, String workingDirectory, ) async { final result = await Process.run( executable, args, workingDirectory: workingDirectory, runInShell: true, ); if (result.exitCode != 0) { stderr.write(result.stderr); exit(result.exitCode); } return true; }