// 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'; 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, '..', 'docs', 'doc', 'flutter', '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(), bool verbose = true, }) async { final ProcessResult result = processManager.runSync(command, stdoutEncoding: utf8, workingDirectory: workingDirectory); if (result.exitCode == 0) { if (verbose) { print('stdout: ${result.stdout}'); } } else { if (verbose) { 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/'); } })(); '''); }