| // Copyright 2014 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:convert'; |
| import 'dart:io'; |
| import 'package:intl/intl.dart'; |
| import 'package:meta/meta.dart'; |
| |
| import 'package:path/path.dart' as path; |
| import 'package:platform/platform.dart' as platform; |
| |
| import 'package:process/process.dart'; |
| |
| const String kDocsRoot = 'dev/docs'; |
| const String kPublishRoot = '$kDocsRoot/doc'; |
| |
| class CommandException implements Exception {} |
| |
| Future<void> main() async { |
| await postProcess(); |
| } |
| |
| /// Post-processes an APIs documentation zip file to modify the footer and version |
| /// strings for commits promoted to either beta or stable channels. |
| Future<void> postProcess() async { |
| final String revision = await gitRevision(fullLength: true); |
| print('Docs revision being processed: $revision'); |
| final Directory tmpFolder = Directory.systemTemp.createTempSync(); |
| final String zipDestination = path.join(tmpFolder.path, 'api_docs.zip'); |
| |
| if (!Platform.environment.containsKey('SDK_CHECKOUT_PATH')) { |
| print('SDK_CHECKOUT_PATH env variable is required for this script'); |
| exit(1); |
| } |
| final String checkoutPath = Platform.environment['SDK_CHECKOUT_PATH']!; |
| final String docsPath = path.join(checkoutPath, 'dev', 'docs'); |
| await runProcessWithValidations( |
| <String>[ |
| 'curl', |
| '-L', |
| 'https://storage.googleapis.com/flutter_infra_release/flutter/$revision/api_docs.zip', |
| '--output', |
| zipDestination, |
| '--fail', |
| ], |
| docsPath, |
| ); |
| |
| // Unzip to docs folder. |
| await runProcessWithValidations( |
| <String>[ |
| 'unzip', |
| '-o', |
| zipDestination, |
| ], |
| docsPath, |
| ); |
| |
| // Generate versions file. |
| await runProcessWithValidations( |
| <String>['flutter', '--version'], |
| docsPath, |
| ); |
| final File versionFile = File('version'); |
| final String version = versionFile.readAsStringSync(); |
| // Recreate footer |
| final String publishPath = path.join(docsPath, 'doc', 'api', 'footer.js'); |
| final File footerFile = File(publishPath)..createSync(recursive: true); |
| createFooter(footerFile, version); |
| } |
| |
| /// Gets the git revision of the current checkout. [fullLength] if true will return |
| /// the full commit hash, if false it will return the first 10 characters only. |
| Future<String> gitRevision({ |
| bool fullLength = false, |
| @visibleForTesting platform.Platform platform = const platform.LocalPlatform(), |
| @visibleForTesting ProcessManager processManager = const LocalProcessManager(), |
| }) async { |
| const int kGitRevisionLength = 10; |
| |
| final ProcessResult gitResult = processManager.runSync(<String>['git', 'rev-parse', 'HEAD']); |
| if (gitResult.exitCode != 0) { |
| throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}'; |
| } |
| final String gitRevision = (gitResult.stdout as String).trim(); |
| if (fullLength) { |
| return gitRevision; |
| } |
| return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision; |
| } |
| |
| /// Wrapper function to run a subprocess checking exit code and printing stderr and stdout. |
| /// [executable] is a string with the script/binary to execute, [args] is the list of flags/arguments |
| /// and [workingDirectory] is as string to the working directory where the subprocess will be run. |
| Future<void> runProcessWithValidations( |
| List<String> command, |
| String workingDirectory, { |
| @visibleForTesting ProcessManager processManager = const LocalProcessManager(), |
| }) async { |
| final ProcessResult result = |
| processManager.runSync(command, stdoutEncoding: utf8, workingDirectory: workingDirectory); |
| if (result.exitCode == 0) { |
| print('Stdout: ${result.stdout}'); |
| } else { |
| print('StdErr: ${result.stderr}'); |
| throw CommandException(); |
| } |
| } |
| |
| /// Get the name of the release branch. |
| /// |
| /// On LUCI builds, the git HEAD is detached, so first check for the env |
| /// variable "LUCI_BRANCH"; if it is not set, fall back to calling git. |
| Future<String> getBranchName({ |
| @visibleForTesting platform.Platform platform = const platform.LocalPlatform(), |
| @visibleForTesting ProcessManager processManager = const LocalProcessManager(), |
| }) async { |
| final RegExp gitBranchRegexp = RegExp(r'^## (.*)'); |
| final String? luciBranch = platform.environment['LUCI_BRANCH']; |
| if (luciBranch != null && luciBranch.trim().isNotEmpty) { |
| return luciBranch.trim(); |
| } |
| final ProcessResult gitResult = processManager.runSync(<String>['git', 'status', '-b', '--porcelain']); |
| if (gitResult.exitCode != 0) { |
| throw 'git status exit with non-zero exit code: ${gitResult.exitCode}'; |
| } |
| final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch((gitResult.stdout as String).trim().split('\n').first); |
| return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first; |
| } |
| |
| /// Updates the footer of the api documentation with the correct branch and versions. |
| /// [footerPath] is the path to the location of the footer js file and [version] is a |
| /// string with the version calculated by the flutter tool. |
| Future<void> createFooter(File footerFile, String version, |
| {@visibleForTesting String? timestampParam, |
| @visibleForTesting String? branchParam, |
| @visibleForTesting String? revisionParam}) async { |
| final String timestamp = timestampParam ?? DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); |
| final String gitBranch = branchParam ?? await getBranchName(); |
| final String revision = revisionParam ?? await gitRevision(); |
| final String gitBranchOut = gitBranch.isEmpty ? '' : '• $gitBranch'; |
| footerFile.writeAsStringSync(''' |
| (function() { |
| var span = document.querySelector('footer>span'); |
| if (span) { |
| span.innerText = 'Flutter $version • $timestamp • $revision $gitBranchOut'; |
| } |
| var sourceLink = document.querySelector('a.source-link'); |
| if (sourceLink) { |
| sourceLink.href = sourceLink.href.replace('/master/', '/$revision/'); |
| } |
| })(); |
| '''); |
| } |