diff --git a/firka/codegen-lock.yaml b/firka/codegen-lock.yaml new file mode 100644 index 0000000..bba4228 --- /dev/null +++ b/firka/codegen-lock.yaml @@ -0,0 +1,21 @@ +icons: + "flutter_launcher_icons.yaml": "c600507ca0df7cebd0f708124842512a14ed3d597b779176200d6ba25b1335b1" + "pubspec.yaml": "57b4c0a018bc425e4b28d8942dca6071371d1801d81f36951c8c37a894a38987" + "assets/images/logos/colored_logo.webp": "4b4fa99d144fe6694aa4487ba1b26aeecafae41e3c877836cd7da28d61a77983" + "assets/images/logos/monochrome_logo.png": "188d2b0a64c70323b09bcee721663d6698fb557066f20ddaec97bba6869c1c6c" + "assets/images/logos/colored_logo_without_mustache.png": "d11cff9f38985885873bfdd2d84e61f8fab03803eada94d4caac1545ef3685f3" + "assets/images/logos/colored_logo_only_mustache.png": "bad6220c11bdfb1dfe04e5173bd2ebedd3999689d4b3a68fc63dc520c96dd33b" +l10n: + "l10n.yml": "a57bc304cac4a2b0235593586f17f400a5165d67fc9aadeaa11893cfa36ee082" + "lib/l10n/app_de.arb": "55f030b312cc07ff05cdc3d6ee10ef9bdec3243b507225e9a47196444518d955" + "lib/l10n/app_en.arb": "cbad6dd2485a983e399cce97371c19089b9110d30536488c14a7ea709c7b6ead" + "lib/l10n/app_hu.arb": "17077ec76b68ed03796a264b99e4dba9e6ddd532e27a92d8fb237ea6f211f757" +isar: + "lib/data/models/app_settings_model.dart": "5eb5af345f1347f104257f0999763650fe2673f9da1754bd12d3f756fe5c9723" + "lib/data/models/generic_cache_model.dart": "79151d0467fb5d40c532eaaa08ad7c7e24a34304199280fbf49cf6e5adcce6bc" + "lib/data/models/homework_cache_model.dart": "45789970b27d5790cdc54c292ef2f5feaa5f4e293b8a8862fd676d5eb3e25d29" + "lib/data/models/timetable_cache_model.dart": "b972bf51e399f8d20d4f9ad660082d4cc4a9798df9ac9d6ec9ef6ac640205572" + "lib/data/models/token_model.dart": "8c957cd07e473827d78fd8fd4fb6c1336b636a69c25c93618e1e7f94b7cf0683" +splash: + "flutter_native_splash.yaml": "0fd4a85d6f950d97298e99916927649940ffcfdadfc136ceee126fed0dbaa9f2" + "assets/images/logos/splash.png": "88fbebc3d686cb9095bcce362029b69978b1b14270e465e91d962b1425db1152" diff --git a/firka/pubspec.yaml b/firka/pubspec.yaml index 6cf647b..0ae6d96 100644 --- a/firka/pubspec.yaml +++ b/firka/pubspec.yaml @@ -62,6 +62,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 + yaml: ^3.1.2 isar_community_generator: 3.3.0 android_notification_icons: ^0.0.1 integration_test: diff --git a/firka/scripts/codegen.dart b/firka/scripts/codegen.dart index 032c5ad..aa6bfea 100644 --- a/firka/scripts/codegen.dart +++ b/firka/scripts/codegen.dart @@ -1,42 +1,55 @@ import 'dart:io'; +import 'package:crypto/crypto.dart'; import 'package:image/image.dart' as img; 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)) { + final inputs = _isarInputs(root); + final hashes = _computeHashes(root, inputs); stdout.writeln( 'Isar generated dart files out of date, running build_runner...', ); await _run('dart', ['run', 'build_runner', 'build'], root); + _updateLockWithHashes(root, 'isar', hashes); ran = true; } if (_splashOutOfDate(root)) { + final inputs = _splashInputs(root); await _generateAndroid12SplashImage(root); stdout.writeln( 'Splash out of date, running flutter_native_splash:create...', ); await _run('dart', ['run', 'flutter_native_splash:create'], root); + _updateLockWithHashes(root, 'splash', _computeHashes(root, inputs)); ran = true; } @@ -47,7 +60,92 @@ void main() async { String _projectRoot() { final script = p.canonicalize(Platform.script.toFilePath()); - return p.dirname(p.dirname(script)); + 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) { @@ -65,7 +163,7 @@ bool _anyNewerThan(Iterable inputs, File output) { return false; } -bool _iconsOutOfDate(String root) { +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.webp')); @@ -78,25 +176,25 @@ bool _iconsOutOfDate(String root) { 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(); +} - final inputs = [ - config, - pubspec, - imagePath, - monochrome, - background, - foreground, - ].where((f) => f.existsSync()).map((f) => File(p.canonicalize(f.path))); +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', ), ); - return _anyNewerThan(inputs, output); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'icons', inputs, _readLock(root)); } -bool _l10nOutOfDate(String root) { +List _l10nInputs(String root) { final l10nDir = p.join(root, 'lib/l10n'); final l10nYml = File(p.join(root, 'l10n.yml')); final arbs = Directory(l10nDir) @@ -105,41 +203,60 @@ bool _l10nOutOfDate(String root) { .where((f) => f.path.endsWith('.arb')) .map((f) => File(p.canonicalize(f.path))) .toList(); - final inputs = [l10nYml, ...arbs].where((f) => f.existsSync()).cast(); - final output = File(p.join(root, 'lib/l10n/app_localizations.dart')); - return _anyNewerThan(inputs, output); + return [l10nYml, ...arbs].where((f) => f.existsSync()).cast().toList(); } -bool _isarOutOfDate(String root) { - final modelsDir = p.join(root, 'lib/data/models'); - if (!Directory(modelsDir).existsSync()) return false; +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; +} - final baseName = p.basenameWithoutExtension(entity.path); - final gPath = p.join(modelsDir, '$baseName.g.dart'); - final dartFile = File(p.canonicalize(entity.path)); - final gFile = File(gPath); - if (_anyNewerThan([dartFile], gFile)) return true; +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 _splashOutOfDate(String root) { +List _splashInputs(String root) { final config = File(p.join(root, 'flutter_native_splash.yaml')); final splashImage = File(p.join(root, 'assets/images/logos/splash.png')); - final inputs = [config, splashImage] + return [config, splashImage] .where((f) => f.existsSync()) .map((f) => File(p.canonicalize(f.path))) .toList(); +} + +bool _splashOutOfDate(String root) { + final inputs = _splashInputs(root); if (inputs.isEmpty) return false; final output = File( p.join(root, 'android/app/src/main/res/drawable/launch_background.xml'), ); - return _anyNewerThan(inputs, output); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'splash', inputs, _readLock(root)); } Future _generateAndroid12SplashImage(String root) async { diff --git a/firka_wear/codegen-lock.yaml b/firka_wear/codegen-lock.yaml new file mode 100644 index 0000000..f80d03c --- /dev/null +++ b/firka_wear/codegen-lock.yaml @@ -0,0 +1,18 @@ +icons: + "flutter_launcher_icons.yaml": "2c1bf9056dfe8db94333143643d2b46308fa332e08de9eda62046a941e83aaaa" + "pubspec.yaml": "6be6ac0844c8554f0e2d3eb4d60adf3debae1b29528b4cfba2023d0ccc5e33bf" + "assets/images/logos/colored_logo.png": "ff9c3452b1b0ed07ffa9067fa4cf4dae45dad3e46f5cb6ef4a62ac8c05d8c080" + "assets/images/logos/monochrome_logo.png": "188d2b0a64c70323b09bcee721663d6698fb557066f20ddaec97bba6869c1c6c" + "assets/images/logos/colored_logo_without_mustache.png": "d11cff9f38985885873bfdd2d84e61f8fab03803eada94d4caac1545ef3685f3" + "assets/images/logos/colored_logo_only_mustache.png": "bad6220c11bdfb1dfe04e5173bd2ebedd3999689d4b3a68fc63dc520c96dd33b" +l10n: + "l10n.yml": "a57bc304cac4a2b0235593586f17f400a5165d67fc9aadeaa11893cfa36ee082" + "lib/l10n/app_de.arb": "55f030b312cc07ff05cdc3d6ee10ef9bdec3243b507225e9a47196444518d955" + "lib/l10n/app_en.arb": "efac3f14d8ecc3e278f80a3e5aff599a88e408d2e30ff9e30f889978f465823a" + "lib/l10n/app_hu.arb": "a7f61bf4452a639d61c350f6674fdb5fd424f9ab31a195a200d446763fa8b396" +isar: + "lib/data/models/app_settings_model.dart": "2bf4d089ccfcb73edbca5b2d5757e1e698ddde2b8783d212a870aac3157fbb5b" + "lib/data/models/generic_cache_model.dart": "dd9979a4f0ba37ce5fd733bf0966088a759b5f356d97ea09c65eefffe8984639" + "lib/data/models/homework_cache_model.dart": "911748133c4bcb32bebe40a7c2f6f30d63c030b89a77c6825ec19643d8f8b3c6" + "lib/data/models/timetable_cache_model.dart": "078cbc0c5b1e3f0303a56bfe1e55df7669f0b06687ba399ddcae2df2b565d4c7" + "lib/data/models/token_model.dart": "3dc6211102c00d8382bfaa929e0ca7dedd7b1771c337f4e96d54e47572e5f6e1" diff --git a/firka_wear/pubspec.yaml b/firka_wear/pubspec.yaml index 08f6dd6..f7bd217 100644 --- a/firka_wear/pubspec.yaml +++ b/firka_wear/pubspec.yaml @@ -65,9 +65,11 @@ dependencies: dev_dependencies: build_runner: any + crypto: ^3.0.6 flutter_test: sdk: flutter flutter_lints: ^6.0.0 + yaml: ^3.1.2 isar_community_generator: 3.3.0 android_notification_icons: ^0.0.1 integration_test: diff --git a/firka_wear/scripts/codegen.dart b/firka_wear/scripts/codegen.dart index 190f2bc..6325632 100644 --- a/firka_wear/scripts/codegen.dart +++ b/firka_wear/scripts/codegen.dart @@ -1,32 +1,43 @@ 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)) { + final inputs = _isarInputs(root); + final hashes = _computeHashes(root, inputs); stdout.writeln( 'Isar generated dart files out of date, running build_runner...', ); await _run('dart', ['run', 'build_runner', 'build'], root); + _updateLockWithHashes(root, 'isar', hashes); ran = true; } @@ -37,7 +48,92 @@ void main() async { String _projectRoot() { final script = p.canonicalize(Platform.script.toFilePath()); - return p.dirname(p.dirname(script)); + 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) { @@ -55,7 +151,7 @@ bool _anyNewerThan(Iterable inputs, File output) { return false; } -bool _iconsOutOfDate(String root) { +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')); @@ -68,25 +164,25 @@ bool _iconsOutOfDate(String root) { 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(); +} - final inputs = [ - config, - pubspec, - imagePath, - monochrome, - background, - foreground, - ].where((f) => f.existsSync()).map((f) => File(p.canonicalize(f.path))); +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', ), ); - return _anyNewerThan(inputs, output); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'icons', inputs, _readLock(root)); } -bool _l10nOutOfDate(String root) { +List _l10nInputs(String root) { final l10nDir = p.join(root, 'lib/l10n'); final l10nYml = File(p.join(root, 'l10n.yml')); final arbs = Directory(l10nDir) @@ -95,25 +191,39 @@ bool _l10nOutOfDate(String root) { .where((f) => f.path.endsWith('.arb')) .map((f) => File(p.canonicalize(f.path))) .toList(); - final inputs = [l10nYml, ...arbs].where((f) => f.existsSync()).cast(); - final output = File(p.join(root, 'lib/l10n/app_localizations.dart')); - return _anyNewerThan(inputs, output); + return [l10nYml, ...arbs].where((f) => f.existsSync()).cast().toList(); } -bool _isarOutOfDate(String root) { - final modelsDir = p.join(root, 'lib/data/models'); - if (!Directory(modelsDir).existsSync()) return false; +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; +} - final baseName = p.basenameWithoutExtension(entity.path); - final gPath = p.join(modelsDir, '$baseName.g.dart'); - final dartFile = File(p.canonicalize(entity.path)); - final gFile = File(gPath); - if (_anyNewerThan([dartFile], gFile)) return true; +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; }