| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'dart:convert'; |
| 6 | import 'dart:io'; |
| 7 | import 'package:intl/intl.dart' ; |
| 8 | import 'package:meta/meta.dart' ; |
| 9 | |
| 10 | import 'package:path/path.dart' as path; |
| 11 | import 'package:platform/platform.dart' as platform; |
| 12 | |
| 13 | import 'package:process/process.dart' ; |
| 14 | |
| 15 | class CommandException implements Exception {} |
| 16 | |
| 17 | Future<void> main() async { |
| 18 | await postProcess(); |
| 19 | } |
| 20 | |
| 21 | /// Post-processes an APIs documentation zip file to modify the footer and version |
| 22 | /// strings for commits promoted to either beta or stable channels. |
| 23 | Future<void> postProcess() async { |
| 24 | final String revision = await gitRevision(fullLength: true); |
| 25 | print('Docs revision being processed: $revision' ); |
| 26 | final Directory tmpFolder = Directory.systemTemp.createTempSync(); |
| 27 | final String zipDestination = path.join(tmpFolder.path, 'api_docs.zip' ); |
| 28 | |
| 29 | if (!Platform.environment.containsKey('SDK_CHECKOUT_PATH' )) { |
| 30 | print('SDK_CHECKOUT_PATH env variable is required for this script' ); |
| 31 | exit(1); |
| 32 | } |
| 33 | final String checkoutPath = Platform.environment['SDK_CHECKOUT_PATH' ]!; |
| 34 | final String docsPath = path.join(checkoutPath, 'dev' , 'docs' ); |
| 35 | await runProcessWithValidations(<String>[ |
| 36 | 'curl' , |
| 37 | '-L' , |
| 38 | 'https://storage.googleapis.com/flutter_infra_release/flutter/$revision/api_docs.zip', |
| 39 | '--output', |
| 40 | zipDestination, |
| 41 | '--fail', |
| 42 | ], docsPath); |
| 43 | |
| 44 | // Unzip to docs folder. |
| 45 | await runProcessWithValidations(<String>['unzip', '-o', zipDestination], docsPath); |
| 46 | |
| 47 | // Generate versions file. |
| 48 | await runProcessWithValidations(<String>['flutter', '--version'], docsPath); |
| 49 | final File versionFile = File('version'); |
| 50 | final String version = versionFile.readAsStringSync(); |
| 51 | // Recreate footer |
| 52 | final String publishPath = path.join(docsPath, '..', 'docs', 'doc', 'flutter', 'footer.js'); |
| 53 | final File footerFile = File(publishPath)..createSync(recursive: true); |
| 54 | createFooter(footerFile, version); |
| 55 | } |
| 56 | |
| 57 | /// Gets the git revision of the current checkout. [fullLength] if true will return |
| 58 | /// the full commit hash, if false it will return the first 10 characters only. |
| 59 | Future<String> gitRevision({ |
| 60 | bool fullLength = false, |
| 61 | @visibleForTesting platform.Platform platform = const platform.LocalPlatform(), |
| 62 | @visibleForTesting ProcessManager processManager = const LocalProcessManager(), |
| 63 | }) async { |
| 64 | const int kGitRevisionLength = 10; |
| 65 | |
| 66 | final ProcessResult gitResult = processManager.runSync(<String>['git', 'rev-parse', 'HEAD']); |
| 67 | if (gitResult.exitCode != 0) { |
| 68 | throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}'; |
| 69 | } |
| 70 | final String gitRevision = (gitResult.stdout as String).trim(); |
| 71 | if (fullLength) { |
| 72 | return gitRevision; |
| 73 | } |
| 74 | return gitRevision.length > kGitRevisionLength |
| 75 | ? gitRevision.substring(0, kGitRevisionLength) |
| 76 | : gitRevision; |
| 77 | } |
| 78 | |
| 79 | /// Wrapper function to run a subprocess checking exit code and printing stderr and stdout. |
| 80 | /// [executable] is a string with the script/binary to execute, [args] is the list of flags/arguments |
| 81 | /// and [workingDirectory] is as string to the working directory where the subprocess will be run. |
| 82 | Future<void> runProcessWithValidations( |
| 83 | List<String> command, |
| 84 | String workingDirectory, { |
| 85 | @visibleForTesting ProcessManager processManager = const LocalProcessManager(), |
| 86 | bool verbose = true, |
| 87 | }) async { |
| 88 | final ProcessResult result = processManager.runSync( |
| 89 | command, |
| 90 | stdoutEncoding: utf8, |
| 91 | workingDirectory: workingDirectory, |
| 92 | ); |
| 93 | if (result.exitCode == 0) { |
| 94 | if (verbose) { |
| 95 | print('stdout: ${result.stdout}'); |
| 96 | } |
| 97 | } else { |
| 98 | if (verbose) { |
| 99 | print('stderr: ${result.stderr}'); |
| 100 | } |
| 101 | throw CommandException(); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | /// Get the name of the release branch. |
| 106 | /// |
| 107 | /// On LUCI builds, the git HEAD is detached, so first check for the env |
| 108 | /// variable "LUCI_BRANCH"; if it is not set, fall back to calling git. |
| 109 | Future<String> getBranchName({ |
| 110 | @visibleForTesting platform.Platform platform = const platform.LocalPlatform(), |
| 111 | @visibleForTesting ProcessManager processManager = const LocalProcessManager(), |
| 112 | }) async { |
| 113 | final RegExp gitBranchRegexp = RegExp(r'^## (.*)'); |
| 114 | final String? luciBranch = platform.environment['LUCI_BRANCH']; |
| 115 | if (luciBranch != null && luciBranch.trim().isNotEmpty) { |
| 116 | return luciBranch.trim(); |
| 117 | } |
| 118 | final ProcessResult gitResult = processManager.runSync(<String>[ |
| 119 | 'git', |
| 120 | 'status', |
| 121 | '-b', |
| 122 | '--porcelain', |
| 123 | ]); |
| 124 | if (gitResult.exitCode != 0) { |
| 125 | throw 'git status exit with non-zero exit code: ${gitResult.exitCode}'; |
| 126 | } |
| 127 | final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch( |
| 128 | (gitResult.stdout as String).trim().split('\n').first, |
| 129 | ); |
| 130 | return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first; |
| 131 | } |
| 132 | |
| 133 | /// Updates the footer of the api documentation with the correct branch and versions. |
| 134 | /// [footerPath] is the path to the location of the footer js file and [version] is a |
| 135 | /// string with the version calculated by the flutter tool. |
| 136 | Future<void> createFooter( |
| 137 | File footerFile, |
| 138 | String version, { |
| 139 | @visibleForTesting String? timestampParam, |
| 140 | @visibleForTesting String? branchParam, |
| 141 | @visibleForTesting String? revisionParam, |
| 142 | }) async { |
| 143 | final String timestamp = timestampParam ?? DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); |
| 144 | final String gitBranch = branchParam ?? await getBranchName(); |
| 145 | final String revision = revisionParam ?? await gitRevision(); |
| 146 | final String gitBranchOut = gitBranch.isEmpty ? '' : '• $gitBranch'; |
| 147 | footerFile.writeAsStringSync(''' |
| 148 | (function() { |
| 149 | var span = document.querySelector('footer>span'); |
| 150 | if (span) { |
| 151 | span.innerText = 'Flutter $version • $timestamp • $revision $gitBranchOut'; |
| 152 | } |
| 153 | var sourceLink = document.querySelector('a.source-link'); |
| 154 | if (sourceLink) { |
| 155 | sourceLink.href = sourceLink.href.replace('/master/', '/$revision/'); |
| 156 | } |
| 157 | })(); |
| 158 | '''); |
| 159 | } |
| 160 | |