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