diff --git a/installers/desktop_inno_script.iss b/installers/desktop_inno_script.iss index 42a2d33d..6724f8bd 100644 --- a/installers/desktop_inno_script.iss +++ b/installers/desktop_inno_script.iss @@ -63,4 +63,4 @@ Filename: "{tmp}\vc_redist.x64.exe"; \ Flags: waituntilterminated postinstall skipifsilent unchecked Filename: "{app}\{#MyAppExeName}"; \ Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; \ - Flags: nowait postinstall skipifsilent \ No newline at end of file + Flags: nowait postinstall skipifsilent diff --git a/pubspec.lock b/pubspec.lock index 0e948847..f313d9b8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,7 +18,7 @@ packages: source: hosted version: "3.6.1" args: - dependency: transitive + dependency: "direct dev" description: name: args sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" @@ -342,7 +342,7 @@ packages: source: hosted version: "4.2.0" intl: - dependency: transitive + dependency: "direct dev" description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf diff --git a/pubspec.yaml b/pubspec.yaml index 4e71fdf6..42cd0139 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,8 @@ dependencies: slang_flutter: any dev_dependencies: + args: ^2.5.0 + flutter_lints: ^4.0.0 flutter_test: @@ -72,6 +74,8 @@ dev_dependencies: icons_launcher: ^2.1.3 + intl: ^0.19.0 + simplytranslate: ^2.0.1 yaml: ^3.1.2 diff --git a/scripts/apply_version.dart b/scripts/apply_version.dart new file mode 100755 index 00000000..fdfe70fb --- /dev/null +++ b/scripts/apply_version.dart @@ -0,0 +1,309 @@ +#!/usr/bin/env dart +// ignore_for_file: avoid_print, library_private_types_in_public_api + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:intl/intl.dart'; +import 'package:ricochlime/utils/version.dart' as old_version_file; + +final oldVersion = _AppVersion.fromName(old_version_file.buildName); +late final _AppVersion newVersion; +late final String editor; +late final bool failOnChanges; +late final bool quiet; + +const String dummyChangelog = 'Release_notes_will_be_added_here'; + +enum ErrorCodes { + noError(0), + noVersionSpecified(1), + noEditorFound(5), + changesNeeded(10); + + const ErrorCodes(this.code); + + final int code; +} + +Future main(List args) async { + parseArgs(args); + await findEditor(); + await updateAllFiles(); +} + +void parseArgs(List args) { + final parser = ArgParser() + ..addFlag('major', abbr: 'M', negatable: false, help: 'Bump major version') + ..addFlag('minor', abbr: 'm', negatable: false, help: 'Bump minor version') + ..addFlag('patch', abbr: 'p', negatable: false, help: 'Bump patch version') + ..addOption( + 'custom', + abbr: 'c', + help: 'Use a custom buildName (e.g. 1.2.3) or buildNumber (e.g. 102030)', + ) + ..addFlag( + 'fail-on-changes', + abbr: 'f', + negatable: false, + help: 'Fail if any changes need to be made', + ) + ..addFlag('quiet', abbr: 'q', negatable: false, help: 'Don\'t open editor') + ..addFlag('help', abbr: 'h', negatable: false, help: 'Show help'); + + final results = parser.parse(args); + + failOnChanges = results.flag('fail-on-changes'); + quiet = results.flag('quiet'); + + if (results.flag('help')) { + print(parser.usage); + exit(ErrorCodes.noError.code); + } else if (results.flag('major')) { + newVersion = oldVersion.bumpMajor(); + } else if (results.flag('minor')) { + newVersion = oldVersion.bumpMinor(); + } else if (results.flag('patch')) { + newVersion = oldVersion.bumpPatch(); + } else if (results.option('custom') != null) { + final String custom = results['custom']!; + late final buildNumber = int.tryParse(custom); + if (custom.contains('.')) { + newVersion = _AppVersion.fromName(custom); + } else if (buildNumber != null) { + newVersion = _AppVersion.fromNumber(buildNumber); + } else { + print('Invalid custom version: $custom'); + print(parser.usage); + exit(ErrorCodes.noVersionSpecified.code); + } + } else { + print('No version specified'); + print(parser.usage); + exit(ErrorCodes.noVersionSpecified.code); + } + + print('Bumping version from ${oldVersion.buildName} ' + 'to ${newVersion.buildName}'); +} + +Future findEditor() async { + if (quiet) { + editor = 'echo'; + print('Will not open editor'); + return; + } + + final whichCode = await Process.run('which', ['code']); + if (whichCode.exitCode == 0) { + editor = 'code'; + print('Using Visual Studio Code as editor'); + return; + } + + final env = Platform.environment['EDITOR']; + if (env != null) { + editor = env; + print('Using $editor as editor'); + return; + } + + print('No editor found. Please set the EDITOR environment variable'); + exit(ErrorCodes.noEditorFound.code); +} + +Future updateAllFiles() async { + // update windows installer + await File('installers/desktop_inno_script.iss').replace({ + // e.g. #define MyAppVersion "0.5.5" + RegExp(r'#define MyAppVersion .+'): + '#define MyAppVersion "${newVersion.buildName}"', + }); + + // update windows runner + await File('windows/runner/Runner.rc').replace({ + // e.g. #define VERSION_AS_NUMBER 0,5,5,0 + RegExp(r'#define VERSION_AS_NUMBER .+'): + '#define VERSION_AS_NUMBER ${newVersion.buildNameWithCommas},0', + // e.g. #define VERSION_AS_STRING "0.5.5.0" + RegExp(r'#define VERSION_AS_STRING .+'): + '#define VERSION_AS_STRING "${newVersion.buildName}.0"', + }); + + // update version file + await File('lib/utils/version.dart').replace({ + // e.g. const int buildNumber = 5050; + RegExp(r'buildNumber = .+;'): 'buildNumber = ${newVersion.buildNumber};', + // e.g. const String buildName = '0.5.5'; + RegExp(r'buildName = .+;'): "buildName = '${newVersion.buildName}';", + // e.g. const int buildYear = 2023; + RegExp(r'buildYear = .+;'): 'buildYear = ${DateTime.now().year};', + }); + + // update pubspec + await File('pubspec.yaml').replace({ + // e.g. version: 5.5.0+5050 + RegExp(r'version: .+'): + 'version: ${newVersion.buildName}+${newVersion.buildNumber}', + }); + + // update download link in README + await File('README.md').replace({ + // e.g. [download_windows]: https://github.com/adil192/ricochlime/releases/download/v0.11.0/RicochlimeInstaller_v0.11.0.exe + RegExp(r'\[download_windows\]: .+'): + '[download_windows]: https://github.com/adil192/ricochlime/releases/download/v${newVersion.buildName}/RicochlimeInstaller_v${newVersion.buildName}.exe', + // e.g. [download_appimage]: https://github.com/adil192/ricochlime/releases/download/v0.11.0/Ricochlime-0.11.0-x86_64.AppImage + RegExp(r'\[download_appimage\]: .+'): + '[download_appimage]: https://github.com/adil192/ricochlime/releases/download/v${newVersion.buildName}/Ricochlime-${newVersion.buildName}-x86_64.AppImage', + }); + + // create metadata changelog + final changelogFile = + File('metadata/en-US/changelogs/${newVersion.buildNumber}.txt'); + if (changelogFile.existsSync()) { + print('Changelog file already exists'); + } else { + if (failOnChanges) { + print('Failed: No changelog file found at ${changelogFile.path}'); + exit(ErrorCodes.changesNeeded.code); + } + print('Creating a blank changelog file'); + await changelogFile.writeAsString('• $dummyChangelog\n'); + } + + // update flatpak changelog + final flatpakFile = File('flatpak/com.adilhanney.ricochlime.metainfo.xml'); + if (await flatpakFile.contains(RegExp(RegExp.escape(newVersion.buildName)))) { + print(' tag already exists in flatpak file'); + } else { + if (failOnChanges) { + print('Failed: No release tag found at ${flatpakFile.path}'); + exit(ErrorCodes.changesNeeded.code); + } + print('Adding a new tag to flatpak file'); + final date = DateFormat('yyyy-MM-dd').format(DateTime.now().toUtc()); + final releaseTag = ''' + + https://github.com/adil192/ricochlime/releases/tag/v${newVersion.buildName} + +
    +
  • $dummyChangelog
  • +
+
+
'''; + final flatpakLines = await flatpakFile.readAsLines(); + final index = + flatpakLines.indexWhere((line) => line.contains('')) + 1; + flatpakLines.insert(index, releaseTag); + if (flatpakLines.last.isNotEmpty) flatpakLines.add(''); + await flatpakFile.writeAsString(flatpakLines.join('\n')); + } + + print(''); + print('Make sure to update the changelog files:'); + print(' - ${changelogFile.path}'); + print(' - ${flatpakFile.path}'); + print('And then run:'); + print(' - dart scripts/translate_changelogs.dart'); + print('Next steps:'); + print( + ' - Publish to the App Store: https://appstoreconnect.apple.com/apps/6459539993/appstore'); + print( + ' - Publish to the Amazon Appstore: https://developer.amazon.com/apps-and-games/console/app/list'); + + // open changelog files in editor + if (!quiet) { + await Process.run(editor, [changelogFile.path]); + await Process.run(editor, [flatpakFile.path]); + } +} + +class _AppVersion { + _AppVersion(this.major, this.minor, this.patch, [this.revision = 0]) + : assert(major >= 0 && major < 100), + assert(minor >= 0 && minor < 100), + assert(patch >= 0 && patch < 100), + assert(revision >= 0 && revision < 10); + + factory _AppVersion.fromName(String name) { + final parts = name.split('.'); + assert(parts.length == 3); + return _AppVersion( + int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2])); + } + + factory _AppVersion.fromNumber(int number) { + // rightmost digit is the revision number + final revision = number % 10; + // next 2 digits are patch version + final patch = (number ~/ 10) % 100; + // next 2 digits are minor version + final minor = (number ~/ 1000) % 100; + // next 2 digits are major version + final major = (number ~/ 100000) % 100; + + return _AppVersion(major, minor, patch, revision); + } + + final int major, minor, patch, revision; + + String get buildName => '$major.$minor.$patch'; + String get buildNameWithCommas => '$major,$minor,$patch'; + int get buildNumber => revision + patch * 10 + minor * 1000 + major * 100000; + int get buildNumberWithoutRevision => buildNumber - revision; + + _AppVersion bumpMajor() => _AppVersion(major + 1, 0, 0); + _AppVersion bumpMinor() => _AppVersion(major, minor + 1, 0); + _AppVersion bumpPatch() => _AppVersion(major, minor, patch + 1); + + _AppVersion copyWith({int? major, int? minor, int? patch, int? revision}) => + _AppVersion(major ?? this.major, minor ?? this.minor, patch ?? this.patch, + revision ?? this.revision); + + @override + String toString() => buildName; + + @override + bool operator ==(Object other) => + other is _AppVersion && + major == other.major && + minor == other.minor && + patch == other.patch; + + @override + int get hashCode => buildNumber; +} + +extension on File { + Future contains(RegExp pattern) async { + final content = await readAsString(); + return pattern.hasMatch(content); + } + + Future replace(Map replacements) async { + int matches = 0; + final lines = await readAsLines(); + for (var i = 0; i < lines.length; i++) { + for (final pattern in replacements.keys) { + if (pattern.hasMatch(lines[i])) { + matches++; + final oldLine = lines[i]; + lines[i] = lines[i].replaceFirst(pattern, replacements[pattern]!); + if (failOnChanges && lines[i] != oldLine) { + print('Failed: Changes needed in $path'); + exit(ErrorCodes.changesNeeded.code); + } + } + } + } + if (lines.last.isNotEmpty) lines.add(''); + await writeAsString(lines.join('\n')); + + if (matches >= replacements.length) { + print('Updated $path with all $matches replacements'); + } else { + print('Updated $path with $matches out of ${replacements.length} ' + 'replacements (${replacements.length - matches} missed)'); + } + } +} diff --git a/scripts/apply_version.sh b/scripts/apply_version.sh deleted file mode 100755 index 9c956de3..00000000 --- a/scripts/apply_version.sh +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env bash - -# Fixes 'grep: -P supports only unibyte and UTF-8 locales' -# for Git Bash on Windows -export LC_ALL=en_US.utf8 - -if [[ "$OSTYPE" == "darwin"* ]]; then - # Use GNU grep on macOS - # brew install grep gnu-sed gawk - PATH="/usr/local/opt/grep/libexec/gnubin:$PATH" - PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH" - PATH="/usr/local/opt/gawk/libexec/gnubin:$PATH" -fi - -# Path to an editor executable with which to open -# the files you need to manually edit. -# -# E.g. if you are using VS Code, you can set this to -# EDITOR=$(which code) -# -# If you leave this empty, the script will not open any files. -# This is the same as using the -q flag. -EDITOR=$(which code) - -# get the current version name from lib/utils/version.dart -function get_version_name { - grep -oP "(?<=buildName = ').*(?=')" lib/utils/version.dart -} - -# get the current version code from lib/utils/version.dart -function get_version_code { - grep -oP '(?<=buildNumber = ).*(?=;)' lib/utils/version.dart -} - -function print_help { - echo "This script is used to apply the version to the relevant files." - echo "Usage: $0 [-q]" - echo "e.g. $0 $(get_version_name) $(get_version_code)" - - echo - echo "Options:" - echo " -q: Quiet mode. Doesn't automatically open files that need to be manually edited." - - exit 0 -} - -# help -if [ "$1" == "-h" ] || [ "$1" == "--help" ] || [ "$1" == "" ]; then - print_help -fi - -# check if we have 2-3 arguments -if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then - print_help -fi - -DUMMY_CHANGELOG="Release notes will be added here" -BUILD_NAME=$1 -BUILD_NUMBER=$2 -DATE=$(date +%Y-%m-%d) -YEAR=$(date +%Y) - -# -q flag -if [ "$3" == "-q" ]; then - EDITOR="" -elif [ "$3" != "" ]; then - print_help -fi - -# Check if the editor exists -if [ "$EDITOR" != "" ] && [ ! -f "$EDITOR" ]; then - echo "Editor not found: $EDITOR" - echo "Please set the EDITOR variable in this script to the path of your editor executable," - echo "or use the -q flag to not open any files." - exit 1 -fi - -# check if the build name is valid -if [[ ! $BUILD_NAME =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Invalid build name: $BUILD_NAME" - exit 1 -fi - -# check if the build number is valid -if [[ ! $BUILD_NUMBER =~ ^[0-9]+$ ]]; then - echo "Invalid build number: $BUILD_NUMBER" - exit 1 -fi - -echo "Applying version $BUILD_NAME ($BUILD_NUMBER) ($DATE) to the relevant files..." - -echo " - Updating MyAppVersion in installers/desktop_inno_script.iss" # e.g. #define MyAppVersion "0.5.5" -sed -i "s/#define MyAppVersion .*/#define MyAppVersion \"$BUILD_NAME\"/g" installers/desktop_inno_script.iss - -echo " - Updating buildNumber in lib/utils/version.dart" # e.g. const int buildNumber = 5050; -sed -i "s/buildNumber = .*/buildNumber = $BUILD_NUMBER;/g" lib/utils/version.dart - -echo " - Updating buildName in lib/utils/version.dart" # e.g. const String buildName = "0.5.5"; -sed -i "s/buildName = .*/buildName = '$BUILD_NAME';/g" lib/utils/version.dart - -echo " - Updating buildYear in lib/utils/version.dart" # e.g. const int buildYear = 2023; -sed -i "s/buildYear = .*/buildYear = $YEAR;/g" lib/utils/version.dart - -echo " - Updating version in pubspec.yaml" # e.g. version: 5.5.0+5050 -sed -i "s/version: .*/version: $BUILD_NAME+$BUILD_NUMBER/g" pubspec.yaml - -echo " - Updating VERSION_AS_NUMBER in windows/runner/Runner.rc" # e.g. #define VERSION_AS_NUMBER 0,5,5,0 -BUILD_NAME_WITH_COMMAS=${BUILD_NAME//./,} -sed -i "s/#define VERSION_AS_NUMBER .*/#define VERSION_AS_NUMBER $BUILD_NAME_WITH_COMMAS,0/g" windows/runner/Runner.rc - -echo " - Updating VERSION_AS_STRING in windows/runner/Runner.rc" # e.g. #define VERSION_AS_STRING "0.5.5.0" -sed -i "s/#define VERSION_AS_STRING .*/#define VERSION_AS_STRING \"$BUILD_NAME.0\"/g" windows/runner/Runner.rc - -echo " - Updating Windows download link in README.md" -# e.g. [download_windows]: https://github.com/adil192/ricochlime/releases/download/v0.11.0/RicochlimeInstaller_v0.11.0.exe -sed -i "s|\[download_windows\]: .*|\[download_windows\]: https://github.com/adil192/ricochlime/releases/download/v${BUILD_NAME}/RicochlimeInstaller_v${BUILD_NAME}.exe|g" README.md - -echo " - Updating AppImage download link in README.md" -# e.g. [download_appimage]: https://github.com/adil192/ricochlime/releases/download/v0.11.0/Ricochlime-0.11.0-x86_64.AppImage -sed -i "s|\[download_appimage\]: .*|\[download_appimage\]: https://github.com/adil192/ricochlime/releases/download/v${BUILD_NAME}/Ricochlime-${BUILD_NAME}-x86_64.AppImage|g" README.md - -echo - -CHANGELOG_FILE="metadata/en-US/changelogs/$BUILD_NUMBER.txt" -if [ -f "$CHANGELOG_FILE" ]; then - echo " - Changelog file already exists at $CHANGELOG_FILE" -else - echo " - (*) Creating a blank changelog file at $CHANGELOG_FILE" - echo "• $DUMMY_CHANGELOG" > "$CHANGELOG_FILE" -fi - -CHANGELOG_FILE_LINK="metadata/en-US/changelogs/${BUILD_NUMBER}3.txt" -if [ -f "$CHANGELOG_FILE_LINK" ]; then - echo " - Changelog file already exists at $CHANGELOG_FILE_LINK" -else - echo " - (*) Creating a symlink to the changelog file at $CHANGELOG_FILE_LINK" - ln -s "$CHANGELOG_FILE" "$CHANGELOG_FILE_LINK" -fi - -FLATPAK_FILE="flatpak/com.adilhanney.ricochlime.metainfo.xml" -if grep -q "$BUILD_NAME" "$FLATPAK_FILE"; then - echo " - tag already exists in $FLATPAK_FILE" -else - echo " - (*) Adding tag to $FLATPAK_FILE" - # shellcheck disable=SC1078,SC1079 - RELEASE_TAG="""\ - - https://github.com/adil192/ricochlime/releases/tag/v$BUILD_NAME - -
    -
  • $DUMMY_CHANGELOG
  • -
-
-
\ -""" - awk -v release="$RELEASE_TAG" 'NR==74{print release}1' "$FLATPAK_FILE" > "${FLATPAK_FILE}.tmp" - mv "${FLATPAK_FILE}.tmp" "$FLATPAK_FILE" -fi - -echo -echo "Make sure to update the two changelog files:" -echo " - $CHANGELOG_FILE" -echo " - $FLATPAK_FILE" -echo "And then run:" -echo " - dart scripts/translate_changelogs.dart" -echo "Next steps:" -echo " - Publish to the App Store: https://appstoreconnect.apple.com/apps/6459539993/appstore" -echo " - Publish to the Amazon Appstore: https://developer.amazon.com/apps-and-games/console/app/list" - -if [ "$EDITOR" != "" ]; then - echo - echo "Opening the changelog files in $EDITOR..." - "$EDITOR" "$CHANGELOG_FILE" - "$EDITOR" flatpak/com.adilhanney.ricochlime.metainfo.xml -fi diff --git a/scripts/translate_changelogs.dart b/scripts/translate_changelogs.dart old mode 100644 new mode 100755 index 0ae419a3..8fcd4944 --- a/scripts/translate_changelogs.dart +++ b/scripts/translate_changelogs.dart @@ -1,3 +1,4 @@ +#!/usr/bin/env dart // Run `dart scripts/translate_changelogs.dart` to generate the changelogs. // ignore_for_file: avoid_print @@ -113,6 +114,16 @@ void main() async { // Response might be something like // "Invalid request: request (276) exceeds text limit (250)" + const failurePrefixes = [ + 'Invalid request: request (', + 'None is not supported', + ]; + if (failurePrefixes.any(translatedChangelog.startsWith)) { + print('${' ' * stepPrefix.length} ! Translation invalid, skipping...'); + someTranslationsFailed = true; + continue; + } + const bullet = '•'; if (englishChangelog.contains(bullet) && !translatedChangelog.contains(bullet)) { diff --git a/scripts/translate_missing_translations.dart b/scripts/translate_missing_translations.dart old mode 100644 new mode 100755 index 2ce3fceb..eb10d866 --- a/scripts/translate_missing_translations.dart +++ b/scripts/translate_missing_translations.dart @@ -1,3 +1,4 @@ +#!/usr/bin/env dart // Run `dart scripts/translate_app.dart` to generate the changelogs. // ignore_for_file: avoid_print @@ -100,14 +101,8 @@ Future translateList( if (translated == null || translated == value) continue; try { - await Process.run('dart', [ - 'run', - 'slang', - 'add', - languageCode, - pathToKey, - translated, - ]); + await Process.run( + 'dart', ['run', 'slang', 'add', languageCode, pathToKey, translated]); } catch (e) { print(' Adding translation failed: $e'); errorOccurredInTranslatingTree = true; @@ -186,8 +181,10 @@ void main() async { ); errorOccurredInTranslatingTree = true; - while (errorOccurredInTranslatingTree) { + int attempts = 0; + while (errorOccurredInTranslatingTree && attempts < 5) { errorOccurredInTranslatingTree = false; + attempts++; final useLibreEngine = random.nextBool(); print( diff --git a/test/version_test.dart b/test/version_test.dart new file mode 100644 index 00000000..0d07738f --- /dev/null +++ b/test/version_test.dart @@ -0,0 +1,43 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:ricochlime/utils/version.dart'; + +const String dummyChangelog = 'Release_notes_will_be_added_here'; + +void main() { + test('Does apply_version.dart find changes needed?', () async { + final result = await Process.run('./scripts/apply_version.dart', [ + '--custom', + buildName, + '--fail-on-changes', + '--quiet', + ]); + printOnFailure(result.stdout); + printOnFailure(result.stderr); + + final exitCode = result.exitCode; + if (exitCode != 0 && exitCode != 10) { + throw Exception('Unexpected exit code: $exitCode'); + } + expect(exitCode, isNot(equals(10)), + reason: 'Changes needed to be made. ' + 'Please re-run `./scripts/apply_version.dart`'); + }); + + test('Check for dummy text in changelogs', () async { + final File androidMetadata = + File('metadata/en-US/changelogs/$buildNumber.txt'); + expect(androidMetadata.existsSync(), true); + final String androidMetadataContents = await androidMetadata.readAsString(); + expect(androidMetadataContents.contains(dummyChangelog), false, + reason: 'Dummy text found in Android changelog'); + + final File flatpakMetadata = + File('flatpak/com.adilhanney.ricochlime.metainfo.xml'); + expect(flatpakMetadata.existsSync(), true); + final String flatpakMetadataContents = await flatpakMetadata.readAsString(); + expect(flatpakMetadataContents.contains(dummyChangelog), false, + reason: 'Dummy text found in Flatpak changelog'); + }); +}