post_process_docs.dart 5.82 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
// 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/');
  }
})();
''');
}