From f41c1644d8efacb08a0fa1bbce12ab2bc0f6edc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Wed, 21 Jun 2023 19:20:06 +0200 Subject: [PATCH] Enabling pre-push checks on Windows (flutter/engine#36123) Re-submit the changes to enable windows pre-push checks. This patch changes how `ci/bin/format.dart` generate diffs from `diff` and `patch` commands to `git diff` and `git apply` in order to have a common method for these operations on all platforms. Windows installations don't have diff and patch commands available by default and many implementations which provide such commands work differently than the UN*X tools. Git however works consistently across all platforms. Additionally, this patch also changes the python executable in some of the pre-push components affected by this to `vpython3` to continue the effort started at flutter/flutter#108474 and I also removed the `--no-sound-null-safety` parameter in the ci/format.sh, ci/format.bat files NOTE: Since the original patch caused some issues, I suggest that this should be tested more carefully before it is merged. ### Issues fixed by this PR * flutter/flutter#108122 * flutter/flutter#107920 * flutter/flutter#86506 * flutter/flutter#106615 ### [flutter/tests] repo impact None. writing and running engine tests. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- engine/src/flutter/ci/bin/format.dart | 168 +++++++++---- engine/src/flutter/ci/format.bat | 11 +- engine/src/flutter/ci/pubspec.yaml | 13 + engine/src/flutter/ci/test/format_test.dart | 227 ++++++++++++++++++ engine/src/flutter/testing/run_tests.py | 27 +++ engine/src/flutter/tools/githooks/pre-push | 2 +- engine/src/flutter/tools/githooks/setup.py | 1 - .../flutter/tools/githooks/windows/pre-push | 5 +- engine/src/flutter/tools/yapf.bat | 16 ++ engine/src/flutter/tools/yapf.sh | 8 + 10 files changed, 426 insertions(+), 52 deletions(-) create mode 100644 engine/src/flutter/ci/test/format_test.dart create mode 100644 engine/src/flutter/tools/yapf.bat diff --git a/engine/src/flutter/ci/bin/format.dart b/engine/src/flutter/ci/bin/format.dart index c67f9c0d7e..1e4de1455e 100644 --- a/engine/src/flutter/ci/bin/format.dart +++ b/engine/src/flutter/ci/bin/format.dart @@ -6,9 +6,6 @@ // // Run with --help for usage. -// TODO(gspencergoog): Support clang formatting on Windows. -// TODO(gspencergoog): Support Java formatting on Windows. - import 'dart:io'; import 'package:args/args.dart'; @@ -84,14 +81,9 @@ String formatCheckToName(FormatCheck check) { } List formatCheckNames() { - List allowed; - if (!Platform.isWindows) { - allowed = FormatCheck.values; - } else { - allowed = [FormatCheck.gn, FormatCheck.whitespace]; - } - return allowed - .map((FormatCheck check) => check.toString().replaceFirst('$FormatCheck.', '')) + return FormatCheck.values + .map((FormatCheck check) => + check.toString().replaceFirst('$FormatCheck.', '')) .toList(); } @@ -226,7 +218,7 @@ abstract class FormatChecker { ); final List jobs = patches.map((String patch) { return WorkerJob( - ['patch', '-p0'], + ['git', 'apply', '--ignore-space-change'], stdinRaw: codeUnitsAsStream(patch.codeUnits), ); }).toList(); @@ -318,6 +310,8 @@ class ClangFormatChecker extends FormatChecker { clangOs = 'linux-x64'; } else if (Platform.isMacOS) { clangOs = 'mac-x64'; + } else if (Platform.isWindows) { + clangOs = 'windows-x64'; } else { throw FormattingException( "Unknown operating system: don't know how to run clang-format here."); @@ -401,9 +395,23 @@ class ClangFormatChecker extends FormatChecker { await for (final WorkerJob completedJob in completedClangFormats) { if (completedJob.result.exitCode == 0) { diffJobs.add( - WorkerJob(['diff', '-u', completedJob.command.last, '-'], + WorkerJob([ + 'git', + 'diff', + '--no-index', + '--no-color', + '--ignore-cr-at-eol', + '--', + completedJob.command.last, + '-', + ], stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw)), ); + } else { + final String formatterCommand = completedJob.command.join(' '); + error("Formatter command '$formatterCommand' failed with exit code " + '${completedJob.result.exitCode}. Command output follows:\n\n' + '${completedJob.result.output}'); } } final ProcessPool diffPool = ProcessPool( @@ -425,9 +433,11 @@ class ClangFormatChecker extends FormatChecker { ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); stdout.writeln('To fix, run:'); stdout.writeln(); - stdout.writeln('patch -p0 <((WorkerJob job) { - return job.result.stdout; + return job.result.stdout + .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}') + .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}'); }).toList(); } } @@ -536,16 +548,30 @@ class JavaFormatChecker extends FormatChecker { processRunner: _processRunner, printReport: namedReport('Java format'), ); - final Stream completedClangFormats = formatPool.startWorkers(formatJobs); + final Stream completedJavaFormats = formatPool.startWorkers(formatJobs); final List diffJobs = []; - await for (final WorkerJob completedJob in completedClangFormats) { + await for (final WorkerJob completedJob in completedJavaFormats) { if (completedJob.result.exitCode == 0) { diffJobs.add( WorkerJob( - ['diff', '-u', completedJob.command.last, '-'], + [ + 'git', + 'diff', + '--no-index', + '--no-color', + '--ignore-cr-at-eol', + '--', + completedJob.command.last, + '-', + ], stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw), ), ); + } else { + final String formatterCommand = completedJob.command.join(' '); + error("Formatter command '$formatterCommand' failed with exit code " + '${completedJob.result.exitCode}. Command output follows:\n\n' + '${completedJob.result.output}'); } } final ProcessPool diffPool = ProcessPool( @@ -567,9 +593,11 @@ class JavaFormatChecker extends FormatChecker { ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); stdout.writeln('To fix, run:'); stdout.writeln(); - stdout.writeln('patch -p0 <((WorkerJob job) { - return job.result.stdout; + return job.result.stdout + .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}') + .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}'); }).toList(); } } @@ -627,45 +657,90 @@ class GnFormatChecker extends FormatChecker { final List cmd = [ gnBinary.path, 'format', - if (!fixing) '--dry-run', + if (!fixing) '--stdin', ]; final List jobs = []; for (final String file in filesToCheck) { - jobs.add(WorkerJob([...cmd, file])); + if (fixing) { + jobs.add(WorkerJob( + [...cmd, file], + name: [...cmd, file].join(' '), + )); + } else { + final WorkerJob job = WorkerJob( + cmd, + stdinRaw: codeUnitsAsStream( + File(path.join(repoDir.absolute.path, file)).readAsBytesSync(), + ), + name: [...cmd, file].join(' '), + ); + jobs.add(job); + } } final ProcessPool gnPool = ProcessPool( processRunner: _processRunner, printReport: namedReport('gn format'), ); - final List completedJobs = await gnPool.runToCompletion(jobs); - reportDone(); - final List incorrect = []; - for (final WorkerJob job in completedJobs) { - if (job.result.exitCode == 2) { - incorrect.add(' ${job.command.last}'); - } - if (job.result.exitCode == 1) { - // GN has exit code 1 if it had some problem formatting/checking the - // file. - throw FormattingException( - 'Unable to format ${job.command.last}:\n${job.result.output}', + final Stream completedJobs = gnPool.startWorkers(jobs); + final List diffJobs = []; + await for (final WorkerJob completedJob in completedJobs) { + if (completedJob.result.exitCode == 0) { + diffJobs.add( + WorkerJob( + [ + 'git', + 'diff', + '--no-index', + '--no-color', + '--ignore-cr-at-eol', + '--', + completedJob.name.split(' ').last, + '-' + ], + stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw), + ), ); + } else { + final String formatterCommand = completedJob.command.join(' '); + error("Formatter command '$formatterCommand' failed with exit code " + '${completedJob.result.exitCode}. Command output follows:\n\n' + '${completedJob.result.output}'); } } - if (incorrect.isNotEmpty) { - final bool plural = incorrect.length > 1; + final ProcessPool diffPool = ProcessPool( + processRunner: _processRunner, + printReport: namedReport('diff'), + ); + final List completedDiffs = + await diffPool.runToCompletion(diffJobs); + final Iterable failed = completedDiffs.where((WorkerJob job) { + return job.result.exitCode != 0; + }); + reportDone(); + if (failed.isNotEmpty) { + final bool plural = failed.length > 1; if (fixing) { - message('Fixed ${incorrect.length} GN file${plural ? 's' : ''}' + message('Fixed ${failed.length} GN file${plural ? 's' : ''}' ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); } else { - error('Found ${incorrect.length} GN file${plural ? 's' : ''}' - ' which ${plural ? 'were' : 'was'} formatted incorrectly:'); - incorrect.forEach(stderr.writeln); + error('Found ${failed.length} GN file${plural ? 's' : ''}' + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); + stdout.writeln('To fix, run:'); + stdout.writeln(); + stdout.writeln('git apply < main(List arguments) async { allowed: formatCheckNames(), defaultsTo: formatCheckNames(), help: 'Specifies which checks will be performed. Defaults to all checks. ' - 'May be specified more than once to perform multiple types of checks. ' - 'On Windows, only whitespace and gn checks are currently supported.'); + 'May be specified more than once to perform multiple types of checks. '); parser.addFlag('verbose', help: 'Print verbose output.', defaultsTo: verbose); late final ArgResults options; diff --git a/engine/src/flutter/ci/format.bat b/engine/src/flutter/ci/format.bat index 35451118bc..8a12f07833 100644 --- a/engine/src/flutter/ci/format.bat +++ b/engine/src/flutter/ci/format.bat @@ -20,7 +20,16 @@ where /q git || ECHO Error: Unable to find git in your PATH. && EXIT /B 1 SET repo_dir=%SRC_DIR%\flutter SET ci_dir=%repo_dir%\ci -SET dart_sdk_path=%SRC_DIR%\third_party\dart\tools\sdks\dart-sdk + +REM Determine which platform we are on and use the right prebuilt Dart SDK +IF "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( + SET dart_sdk_path=%SRC_DIR%\flutter\prebuilts\windows-x64\dart-sdk +) ELSE IF "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + SET dart_sdk_path=%SRC_DIR%\flutter\prebuilts\windows-arm64\dart-sdk +) ELSE ( + ECHO "Windows x86 (32-bit) is not supported" && EXIT /B 1 +) + SET dart=%dart_sdk_path%\bin\dart.exe cd "%ci_dir%" diff --git a/engine/src/flutter/ci/pubspec.yaml b/engine/src/flutter/ci/pubspec.yaml index 6b09e88641..85639d87f8 100644 --- a/engine/src/flutter/ci/pubspec.yaml +++ b/engine/src/flutter/ci/pubspec.yaml @@ -23,15 +23,26 @@ dependencies: process_runner: any process: any +dev_dependencies: + async_helper: any + expect: any + litetest: any + dependency_overrides: args: path: ../../third_party/dart/third_party/pkg/args async: path: ../../third_party/dart/third_party/pkg/async + async_helper: + path: ../../third_party/dart/pkg/async_helper collection: path: ../../third_party/dart/third_party/pkg/collection + expect: + path: ../../third_party/dart/pkg/expect file: path: ../../third_party/pkg/file/packages/file + litetest: + path: ../testing/litetest meta: path: ../../third_party/dart/pkg/meta path: @@ -42,3 +53,5 @@ dependency_overrides: path: ../../third_party/pkg/process process_runner: path: ../../third_party/pkg/process_runner + smith: + path: ../../third_party/dart/pkg/smith diff --git a/engine/src/flutter/ci/test/format_test.dart b/engine/src/flutter/ci/test/format_test.dart new file mode 100644 index 0000000000..78af163bad --- /dev/null +++ b/engine/src/flutter/ci/test/format_test.dart @@ -0,0 +1,227 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:litetest/litetest.dart'; +import 'package:path/path.dart' as path; +import 'package:process_runner/process_runner.dart'; + +import '../bin/format.dart' as target; + +final io.File script = io.File.fromUri(io.Platform.script).absolute; +final io.Directory repoDir = script.parent.parent.parent; +final ProcessPool pool = ProcessPool( + numWorkers: 1, + processRunner: ProcessRunner(defaultWorkingDirectory: repoDir), +); + +class FileContentPair { + FileContentPair(this.original, this.formatted, this.fileExtension); + + final String original; + final String formatted; + final String fileExtension; +} + +final FileContentPair ccContentPair = FileContentPair( + 'int main(){return 0;}\n', 'int main() {\n return 0;\n}\n', '.cc'); +final FileContentPair hContentPair = + FileContentPair('int\nmain\n()\n;\n', 'int main();\n', '.h'); +final FileContentPair gnContentPair = FileContentPair( + 'test\n(){testvar=true}\n', 'test() {\n testvar = true\n}\n', '.gn'); +final FileContentPair javaContentPair = FileContentPair( + 'class Test{public static void main(String args[]){System.out.println("Test");}}\n', + 'class Test {\n public static void main(String args[]) {\n System.out.println("Test");\n }\n}\n', + '.java'); +final FileContentPair pythonContentPair = FileContentPair( + "if __name__=='__main__':\n sys.exit(\nMain(sys.argv)\n)\n", + "if __name__ == '__main__':\n sys.exit(Main(sys.argv))\n", + '.py'); +final FileContentPair whitespaceContentPair = FileContentPair( + 'int main() {\n return 0; \n}\n', + 'int main() {\n return 0;\n}\n', + '.c'); + +class TestFileFixture { + TestFileFixture(this.type) { + switch (type) { + case target.FormatCheck.clang: + final io.File ccFile = io.File('${repoDir.path}/format_test.cc'); + ccFile.writeAsStringSync(ccContentPair.original); + files.add(ccFile); + + final io.File hFile = io.File('${repoDir.path}/format_test.h'); + hFile.writeAsStringSync(hContentPair.original); + files.add(hFile); + case target.FormatCheck.gn: + final io.File gnFile = io.File('${repoDir.path}/format_test.gn'); + gnFile.writeAsStringSync(gnContentPair.original); + files.add(gnFile); + case target.FormatCheck.java: + final io.File javaFile = io.File('${repoDir.path}/format_test.java'); + javaFile.writeAsStringSync(javaContentPair.original); + files.add(javaFile); + case target.FormatCheck.python: + final io.File pyFile = io.File('${repoDir.path}/format_test.py'); + pyFile.writeAsStringSync(pythonContentPair.original); + files.add(pyFile); + case target.FormatCheck.whitespace: + final io.File whitespaceFile = io.File('${repoDir.path}/format_test.c'); + whitespaceFile.writeAsStringSync(whitespaceContentPair.original); + files.add(whitespaceFile); + } + } + + final target.FormatCheck type; + final List files = []; + + void gitAdd() { + final List args = ['add']; + for (final io.File file in files) { + args.add(file.path); + } + + io.Process.runSync('git', args); + } + + void gitRemove() { + final List args = ['rm', '-f']; + for (final io.File file in files) { + args.add(file.path); + } + io.Process.runSync('git', args); + } + + Iterable getFileContents() { + return files.map((io.File file) { + String content = file.readAsStringSync(); + // Avoid clang-tidy formatting CRLF EOL on Windows + content = content.replaceAll('\r\n', '\n'); + switch (type) { + case target.FormatCheck.clang: + return FileContentPair( + content, + path.extension(file.path) == '.cc' + ? ccContentPair.formatted + : hContentPair.formatted, + path.extension(file.path), + ); + case target.FormatCheck.gn: + return FileContentPair( + content, + gnContentPair.formatted, + path.extension(file.path), + ); + case target.FormatCheck.java: + return FileContentPair( + content, + javaContentPair.formatted, + path.extension(file.path), + ); + case target.FormatCheck.python: + return FileContentPair( + content, + pythonContentPair.formatted, + path.extension(file.path), + ); + case target.FormatCheck.whitespace: + return FileContentPair( + content, + whitespaceContentPair.formatted, + path.extension(file.path), + ); + } + }); + } +} + +void main() { + final String formatterPath = + '${repoDir.path}/ci/format.${io.Platform.isWindows ? 'bat' : 'sh'}'; + + test('Can fix C++ formatting errors', () { + final TestFileFixture fixture = TestFileFixture(target.FormatCheck.clang); + try { + fixture.gitAdd(); + io.Process.runSync(formatterPath, ['--check', 'clang', '--fix'], + workingDirectory: repoDir.path); + + final Iterable files = fixture.getFileContents(); + for (final FileContentPair pair in files) { + expect(pair.original, equals(pair.formatted)); + } + } finally { + fixture.gitRemove(); + } + }); + + test('Can fix GN formatting errors', () { + final TestFileFixture fixture = TestFileFixture(target.FormatCheck.gn); + try { + fixture.gitAdd(); + io.Process.runSync(formatterPath, ['--check', 'gn', '--fix'], + workingDirectory: repoDir.path); + + final Iterable files = fixture.getFileContents(); + for (final FileContentPair pair in files) { + expect(pair.original, equals(pair.formatted)); + } + } finally { + fixture.gitRemove(); + } + }); + + test('Can fix Java formatting errors', () { + final TestFileFixture fixture = TestFileFixture(target.FormatCheck.java); + try { + fixture.gitAdd(); + io.Process.runSync(formatterPath, ['--check', 'java', '--fix'], + workingDirectory: repoDir.path); + + final Iterable files = fixture.getFileContents(); + for (final FileContentPair pair in files) { + expect(pair.original, equals(pair.formatted)); + } + } finally { + fixture.gitRemove(); + } + // TODO(mtolmacs): Fails if Java dependency is unavailable, + // https://github.com/flutter/flutter/issues/129221 + }, skip: true); + + test('Can fix Python formatting errors', () { + final TestFileFixture fixture = TestFileFixture(target.FormatCheck.python); + try { + fixture.gitAdd(); + io.Process.runSync(formatterPath, ['--check', 'python', '--fix'], + workingDirectory: repoDir.path); + + final Iterable files = fixture.getFileContents(); + for (final FileContentPair pair in files) { + expect(pair.original, equals(pair.formatted)); + } + } finally { + fixture.gitRemove(); + } + }); + + test('Can fix whitespace formatting errors', () { + final TestFileFixture fixture = + TestFileFixture(target.FormatCheck.whitespace); + try { + fixture.gitAdd(); + io.Process.runSync( + formatterPath, ['--check', 'whitespace', '--fix'], + workingDirectory: repoDir.path); + + final Iterable files = fixture.getFileContents(); + for (final FileContentPair pair in files) { + expect(pair.original, equals(pair.formatted)); + } + } finally { + fixture.gitRemove(); + } + }); +} diff --git a/engine/src/flutter/testing/run_tests.py b/engine/src/flutter/testing/run_tests.py index 46e398e829..a73182a9cd 100755 --- a/engine/src/flutter/testing/run_tests.py +++ b/engine/src/flutter/testing/run_tests.py @@ -979,6 +979,32 @@ def gather_api_consistency_tests(build_dir): ) +def gather_ci_tests(build_dir): + test_dir = os.path.join(BUILDROOT_DIR, 'flutter', 'ci') + dart_tests = glob.glob('%s/test/*_test.dart' % test_dir) + + run_engine_executable( + build_dir, + os.path.join('dart-sdk', 'bin', 'dart'), + None, + flags=['pub', 'get', '--offline'], + cwd=test_dir, + ) + + for dart_test_file in dart_tests: + opts = [ + '--disable-dart-dev', dart_test_file, + os.path.join(BUILDROOT_DIR, 'flutter') + ] + yield EngineExecutableTask( + build_dir, + os.path.join('dart-sdk', 'bin', 'dart'), + None, + flags=opts, + cwd=test_dir + ) + + def run_engine_tasks_in_parallel(tasks): # Work around a bug in Python. # @@ -1217,6 +1243,7 @@ Flutter Wiki page on the subject: https://github.com/flutter/flutter/wiki/Testin tasks += list(gather_path_ops_tests(build_dir)) tasks += list(gather_const_finder_tests(build_dir)) tasks += list(gather_front_end_server_tests(build_dir)) + tasks += list(gather_ci_tests(build_dir)) tasks += list(gather_dart_tests(build_dir, dart_filter)) run_engine_tasks_in_parallel(tasks) diff --git a/engine/src/flutter/tools/githooks/pre-push b/engine/src/flutter/tools/githooks/pre-push index 4e10ce870a..0f629a08c8 100755 --- a/engine/src/flutter/tools/githooks/pre-push +++ b/engine/src/flutter/tools/githooks/pre-push @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env vpython3 # Copyright 2013 The Flutter Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. diff --git a/engine/src/flutter/tools/githooks/setup.py b/engine/src/flutter/tools/githooks/setup.py index ea8edf467a..cbf15c79e7 100755 --- a/engine/src/flutter/tools/githooks/setup.py +++ b/engine/src/flutter/tools/githooks/setup.py @@ -29,7 +29,6 @@ def Main(argv): githooks = os.path.join(FLUTTER_DIR, 'tools', 'githooks') if IsWindows(): git = 'git.bat' - githooks = os.path.join(githooks, 'windows') result = subprocess.run([ git, 'config', diff --git a/engine/src/flutter/tools/githooks/windows/pre-push b/engine/src/flutter/tools/githooks/windows/pre-push index aa363e3cdf..811a4c9e6c 100644 --- a/engine/src/flutter/tools/githooks/windows/pre-push +++ b/engine/src/flutter/tools/githooks/windows/pre-push @@ -3,8 +3,9 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +YELLOW='' RED='' NC='' BOLD='' -echo "${BOLD}${RED}Warning:${NC} ${BOLD}Pre-push checks not supported on Windows${NC}" -echo "${BOLD}See: https://github.com/flutter/flutter/issues/86506${NC}" +echo "${BOLD}${RED}Warning:${NC} ${BOLD}Pre-push checks are not enabled!${NC}" +echo "${YELLOW}${BOLD}Note:${NC} ${BOLD}Please run 'gclient sync -D' to enable pre-push checks on Windows${NC}" diff --git a/engine/src/flutter/tools/yapf.bat b/engine/src/flutter/tools/yapf.bat new file mode 100644 index 0000000000..587989cedb --- /dev/null +++ b/engine/src/flutter/tools/yapf.bat @@ -0,0 +1,16 @@ +@ECHO off +REM Copyright 2013 The Flutter Authors. All rights reserved. +REM Use of this source code is governed by a BSD-style license that can be +REM found in the LICENSE file. + +REM ---------------------------------- NOTE ---------------------------------- +REM +REM Please keep the logic in this file consistent with the logic in the +REM `yapf.sh` script in the same directory to ensure that it continues to +REM work across all platforms! +REM +REM -------------------------------------------------------------------------- + +SET yapf_path=%~dp0\..\..\third_party\yapf + +cmd /V /C "SET PYTHONPATH=%yapf_path%&& vpython3 %yapf_path%\yapf %*" diff --git a/engine/src/flutter/tools/yapf.sh b/engine/src/flutter/tools/yapf.sh index af6e7021dd..c3cbd5587e 100755 --- a/engine/src/flutter/tools/yapf.sh +++ b/engine/src/flutter/tools/yapf.sh @@ -3,6 +3,14 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +# ---------------------------------- NOTE ---------------------------------- +# +# Please keep the logic in this file consistent with the logic in the +# `yapf.bat` script in the same directory to ensure that it continues to +# work across all platforms! +# +# -------------------------------------------------------------------------- + # Generates objc docs for Flutter iOS libraries. set -e