From d6a3740c56dda0a8cfe2b4832496687ba26b6352 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 25 Jul 2019 15:37:11 -0700 Subject: [PATCH] Declare tasks programatically rather than declaratively This has a few benefits: * It allows us to dynamically choose task dependencies (so for example the Linux standalone package can depend on the native executable task when running on Linux). * Similar tasks can be defined programmatically. * Task names are decoupled from Dart function names, which in turn allows us to avoid manually namespacing the package in favor of encouraging it to be imported with a Dart namespace. * It works around google/grinder.dart#337 and google/grinder.dart#338. --- README.md | 24 +++ doc/github.md | 53 ++++++ doc/standalone.md | 70 ++++++++ lib/cli_pkg.dart | 66 ++------ lib/github.dart | 338 -------------------------------------- lib/src/github.dart | 269 ++++++++++++++++++++++++++++++ lib/src/info.dart | 22 +-- lib/src/standalone.dart | 203 +++++++++++++++++++++++ lib/src/utils.dart | 29 ++-- lib/standalone.dart | 267 ------------------------------ test/descriptor.dart | 2 +- test/github_test.dart | 88 +++++----- test/standalone_test.dart | 55 +++---- 13 files changed, 726 insertions(+), 760 deletions(-) create mode 100644 doc/github.md create mode 100644 doc/standalone.md delete mode 100644 lib/github.dart create mode 100644 lib/src/github.dart create mode 100644 lib/src/standalone.dart delete mode 100644 lib/standalone.dart diff --git a/README.md b/README.md index bea528b..335ca4b 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,27 @@ and non-Dart users alike. It also integrates with Travis CI to make it easy to automatically deploy packages. [Grinder]: https://pub.dev/packages/grinder + +To use this package, import `package:cli_pkg/cli_pkg.dart` and call +[`pkg.addAllTasks()`][] before calling [`grind()`][]: + +[`pkg.addAllTasks()`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/addAllTasks.html +[`grind()`]: https://pub.dev/documentation/grinder/latest/grinder/grind.html + +```dart +import 'package:cli_pkg/cli_pkg.dart' as pkg; +import 'package:grinder/grinder.dart'; + +void main(List args) { + pkg.addAllTasks(); + grind(args); +} +``` + +The following sets of tasks are provided, each of which can also be enabled +individually: + +* [Creating standalone archives for your package.](doc/standalone.md) +* [Uploading standalone archives to GitHub releases.](doc/github.md) + +It's strongly recommended that this package be imported with the prefix `pkg`. diff --git a/doc/github.md b/doc/github.md new file mode 100644 index 0000000..d934ef9 --- /dev/null +++ b/doc/github.md @@ -0,0 +1,53 @@ +These tasks upload [standalone packages][] to GitHub releases. They're enabled +by calling [`pkg.addGithubTasks()`][], which automatically calls +[`pkg.addStandaloneTasks()`][] + +[standalone packages]: standalone.md +[`pkg.addGithubTasks()`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/addGithubTasks.html +[`pkg.addStandaloneTasks()`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/addStandaloneTasks.html + +## `pkg-github-release` + +Uses configuration: [`pkg.humanName`][], [`pkg.version`][], +[`pkg.githubRepo`][], [`pkg.githubReleaseNotes`][] + +[`pkg.humanName`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/humanName.html +[`pkg.version`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/version.html +[`pkg.githubRepo`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/githubRepo.html +[`pkg.githubReleaseNotes`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/githubReleaseNotes.html + +Creates a GitHub release for the current version of this package, without any +files uploaded to it. + +## `pkg-github-$os` + +Depends on: [`pkg-standalone-$os-ia32`, `pkg-standalone-$os-x64`][] + +[`pkg-standalone-$os-ia32`, `pkg-standalone-$os-x64`]: standalone.md#pkg-standalone-os-arch + +Uses configuration: [`pkg.version`][], [`pkg.githubRepo`][], [`pkg.standaloneName`][] + +[`pkg.standaloneName`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/standaloneName.html + +Uploads 32- and 64-bit executable packages for the given operating system +(`linux`, `windows`, or `macos`) to the GitHub release for the current version. + +Any OS's packages can be built and uploaded regardless of the OS running the +task, but if the host OS matches the target OS the 64-bit executable will be +built as a native executable, which is substantially faster. + +This must be invoked after [`pkg-github-release`][], but it doesn't have a +built-in dependency so that different OSs' packages can be built in different +build steps. + +[`pkg-github-release`]: #pkg-github-release + +## `pkg-github-all` + +Depends on: [`pkg-github-release`][], [`pkg-github-linux`, `pkg-github-macos`, +`pkg-github-windows`][]. + +[`pkg-github-linux`, `pkg-github-macos`, `pkg-github-windows`]: #pkg-github-os + +A utility task for creating a release and uploading packages for all operating +systems in the same step. diff --git a/doc/standalone.md b/doc/standalone.md new file mode 100644 index 0000000..1aaf40a --- /dev/null +++ b/doc/standalone.md @@ -0,0 +1,70 @@ +These tasks create self-contained archives containing the Dart VM and snapshots +of the package's executables, which can then be easily distributed. They're the +basis for many other tasks that upload the packages to various package managers. +They're enabled by calling [`pkg.addStandaloneTasks()`][]. + +[`pkg.addStandaloneTasks()`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/addStandaloneTasks.html + +## `pkg-compile-snapshot` + +Uses configuration: [`pkg.entrypoints`][] + +[`pkg.entrypoints`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/entrypoints.html + +Output: `build/$entrypoint.snapshot` + +Compiles each executable in the package to a [kernel snapshot][snapshot]. + +[snapshots]: https://github.com/dart-lang/sdk/wiki/Snapshots + +## `pkg-compile-native` + +Uses configuration: [`pkg.entrypoints`][], [`pkg.version`][] + +[`pkg.version`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/version.html + +Output: `build/$entrypoint.native` + +Compiles each executable in the package to a native code snapshot (what Dart +calls an ["AOT Application snapshot"][snapshot]). This is unavailable on 32-bit +host systems. + +Defines an environment constant named `version` set to [`pkg.version`][] that +can be accessed from within each entrypoint via [`String.fromEnvironment()`][]. + +[`String.fromEnvironment()`]: https://api.dartlang.org/stable/dart-core/String/String.fromEnvironment.html + +## `pkg-standalone-$os-$arch` + +Depends on: [`pkg-compile-snapshot`][] or [`pkg-compile-native`][] + +[`pkg-compile-snapshot`]: #pkg-compile-snapshot +[`pkg-compile-native`]: #pkg-compile-native + +Uses configuration: [`pkg.version`][], [`pkg.standaloneName`][], [`pkg.entrypoints`][] + +[`pkg.standaloneName`]: https://pub.dev/documentation/dart_cli_pkg/latest/cli_pkg/standaloneName.html + +Output: `build/$standaloneName-$version-$os-$arch.(tar.gz|zip)` + +Creates an archive that contains all this package's entrypoints along with the +Dart VM for the given operating system and architecture, with top-level scripts +that can be used to invoke them. + +Any OS's packages can be built regardless of the OS running the task, but if the +host OS matches the target OS *and* the architecture is 64-bit, executables will +be built as native (["AOT"][snapshot]) executables, which are substantially +faster and smaller than the kernel snapshots that are generated otherwise. + +This produces a ZIP file in Windows, and a gzipped TAR file on Linux and Mac OS. + +## `pkg-standalone-all` + +Depends on: [`pkg-standalone-linux-ia32`, `pkg-standalone-linux-x64`, +`pkg-standalone-macos-ia32`, `pkg-standalone-macos-x64`, +`pkg-standalone-windows-ia32`, `pkg-standalone-windows-x64`][] + +[`pkg-standalone-linux-ia32`, `pkg-standalone-linux-x64`, `pkg-standalone-macos-ia32`, `pkg-standalone-macos-x64`, `pkg-standalone-windows-ia32`, `pkg-standalone-windows-x64`]: #pkg-standalone-os-arch + +A utility task for creating a packages for all operating systems in the same +step. diff --git a/lib/cli_pkg.dart b/lib/cli_pkg.dart index b0984ef..77516dc 100644 --- a/lib/cli_pkg.dart +++ b/lib/cli_pkg.dart @@ -12,57 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:grinder/grinder.dart'; - -import 'github.dart' as github; -import 'standalone.dart' as standalone; - -export 'src/info.dart' hide pubspec; - -// Manually export tasks to work around google/grinder.dart#337 - -@Task('Build Dart script snapshot(s).') -void pkgCompileSnapshot() => standalone.pkgCompileSnapshot(); - -@Task('Build Dart native executable(s).') -void pkgCompileNative() => standalone.pkgCompileNative(); - -@Depends(pkgCompileSnapshot) -@Task('Build a standalone 32-bit package for Linux.') -Future pkgStandaloneLinuxIa32() => standalone.pkgStandaloneLinuxIa32(); - -@Task('Build a standalone 64-bit package for Linux.') -Future pkgStandaloneLinuxX64() => standalone.pkgStandaloneLinuxX64(); - -@Depends(pkgCompileSnapshot) -@Task('Build a standalone 32-bit package for Mac OS.') -Future pkgStandaloneMacOsIa32() => standalone.pkgStandaloneMacOsIa32(); - -@Task('Build a standalone 64-bit package for Mac OS.') -Future pkgStandaloneMacOsX64() => standalone.pkgStandaloneMacOsX64(); - -@Depends(pkgCompileSnapshot) -@Task('Build a standalone 32-bit package for Windows.') -Future pkgStandaloneWindowsIa32() => - standalone.pkgStandaloneWindowsIa32(); - -@Task('Build a standalone 64-bit package for Windows.') -Future pkgStandaloneWindowsX64() => standalone.pkgStandaloneWindowsX64(); - -@Task('Build all standalone packages.') -Future pkgStandaloneAll() => standalone.pkgStandaloneAll(); - -@Task('Create a GitHub release, without executables.') -Future pkgGithubRelease() => github.pkgGithubRelease(); - -@Task('Release Linux executables to GitHub.') -Future pkgGithubLinux() => github.pkgGithubLinux(); - -@Task('Release Mac OS executables to GitHub.') -Future pkgGithubMacOs() => pkgGithubMacOs(); - -@Task('Release Windows executables to GitHub.') -Future pkgGithubWindows() => pkgGithubWindows(); - -@Task('Create a GitHub release with all executables.') -Future pkgGithubAll() => pkgGithubAll(); +import 'src/github.dart'; +import 'src/standalone.dart'; + +export 'src/info.dart'; +export 'src/github.dart'; +export 'src/standalone.dart'; + +/// Enables all tasks from the `cli_pkg` package. +void addAllTasks() { + addGithubTasks(); + addStandaloneTasks(); +} diff --git a/lib/github.dart b/lib/github.dart deleted file mode 100644 index 517ffbe..0000000 --- a/lib/github.dart +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:charcode/charcode.dart'; -import 'package:grinder/grinder.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:string_scanner/string_scanner.dart'; - -import 'standalone.dart'; -import 'src/info.dart'; -import 'src/utils.dart'; - -/// The GitHub repository slug (for example, `username/repo`) to which to upload -/// package releases. -/// -/// By default, this determines the repo from Git's `origin` remote, failing -/// that, from the pubspec's `homepage` field. If neither of those is a valid -/// Git URL, this must be set explicitly. -String get pkgGithubRepo { - _pkgGithubRepo ??= _repoFromOrigin() ?? _repoFromPubspec(); - if (_pkgGithubRepo != null) return _pkgGithubRepo; - - fail("pkgGithubRepo must be set to deploy to GitHub."); -} - -set pkgGithubRepo(String value) => _pkgGithubRepo = value; -String _pkgGithubRepo; - -/// Returns the GitHub repo name from the Git configuration's -/// `remote.origin.url` field. -String _repoFromOrigin() { - try { - var result = Process.runSync("git", ["config", "remote.origin.url"]); - if (result.exitCode != 0) return null; - var origin = (result.stdout as String).trim(); - return origin.startsWith("http") ? _parseHttp(origin) : _parseGit(origin); - } on IOException { - return null; - } -} - -/// Returns the GitHub repo name from the pubspec's `homepage` field. -String _repoFromPubspec() => - pubspec.homepage == null ? null : _parseHttp(pubspec.homepage); - -/// Parses a GitHub repo name from an SSH reference or a `git://` URL. -/// -/// Returns `null` if it couldn't be parsed. -String _parseGit(String url) { - var match = RegExp(r"^(git@github\.com:|git://github\.com/)" - r"(?[^/]+/[^/]+?)(\.git)?$") - .firstMatch(url); - return match == null ? null : match.namedGroup('repo'); -} - -/// Parses a GitHub repo name from an HTTP or HTTPS URL. -/// -/// Returns `null` if it couldn't be parsed. -String _parseHttp(String url) { - var match = RegExp(r"^https?://github\.com/([^/]+/[^/]+)").firstMatch(url); - return match == null ? null : match[1]; -} - -/// The GitHub username to use when creating releases and making other changes. -/// -/// By default, this comes from the `GITHUB_USER` environment variable. -String get pkgGithubUser { - _pkgGithubUser ??= Platform.environment["GITHUB_USER"]; - if (_pkgGithubUser != null) return _pkgGithubUser; - - fail("pkgGithubUser must be set to deploy to GitHub."); -} - -set pkgGithubUser(String value) => _pkgGithubUser = value; -String _pkgGithubUser; - -/// The GitHub password or authentication token to use when creating releases -/// and making other changes. -/// -/// **Do not check this in directly.** This should only come from secure -/// sources. -/// -/// This can be either the username itself, a [personal access token][], or an -/// OAuth token. -/// -/// [personal access token]: https://github.com/settings/tokens -/// -/// By default, this comes from the `GITHUB_PASSWORD` environment variable if it -/// exists, or `GITHUB_TOKEN` otherwise. -String get pkgGithubPassword { - _pkgGithubPassword ??= Platform.environment["GITHUB_PASSWORD"] ?? - Platform.environment["GITHUB_TOKEN"]; - if (_pkgGithubPassword != null) return _pkgGithubPassword; - - fail("pkgGithubPassword must be set to deploy to GitHub."); -} - -set pkgGithubPassword(String value) => _pkgGithubPassword = value; -String _pkgGithubPassword; - -/// Returns the HTTP basic authentication Authorization header from the -/// environment. -String get _authorization => - "Basic ${base64.encode(utf8.encode("$pkgGithubUser:$pkgGithubPassword"))}"; - -/// The Markdown-formatted release notes to use for the GitHub release. -/// -/// By default, this looks for a `CHANGELOG.md` file at the root of the -/// repository and uses the portion between the first and second level-2 `##` -/// headers. It throws a [FormatException] if the CHANGELOG doesn't begin with -/// `##` followed by [pkgVersion]. -String get pkgGithubReleaseNotes { - if (_pkgGithubReleaseNotes != null) return _pkgGithubReleaseNotes; - if (!File("CHANGELOG.md").existsSync()) return null; - - _pkgGithubReleaseNotes = _lastChangelogSection() + - "\n\n" - "See the [full changelog](https://github.com/$pkgGithubRepo/blob/" - "master/CHANGELOG.md#${pkgVersion.toString().replaceAll(".", "")}) " - "for changes in earlier releases."; - return _pkgGithubReleaseNotes; -} - -set pkgGithubReleaseNotes(String value) => _pkgGithubReleaseNotes = value; -String _pkgGithubReleaseNotes; - -/// Creates a GitHub release for [pkgVersion] of this package. -/// -/// This creates the release on the [pkgGithubRepo] repository, using -/// [pkgHumanName] as the release name and [pkgGithubReleaseNotes] as the -/// release notes. -@Task('Create a GitHub release, without executables.') -Future pkgGithubRelease({http.Client client}) async { - var response = await withClient(client, (client) { - return client.post( - url("https://api.github.com/repos/$pkgGithubRepo/releases"), - headers: { - "content-type": "application/json", - "authorization": _authorization - }, - body: jsonEncode({ - "tag_name": pkgVersion.toString(), - "name": "$pkgHumanName $pkgVersion", - "prerelease": pkgVersion.isPreRelease, - if (pkgGithubReleaseNotes != null) "body": pkgGithubReleaseNotes - })); - }); - - if (response.statusCode != 201) { - fail("${response.statusCode} error creating release:\n${response.body}"); - } else { - log("Released $pkgHumanName $pkgVersion to GitHub."); - } -} - -/// A regular expression that matches a Markdown code block. -final _codeBlock = RegExp(" *```"); - -/// Returns the most recent section in the CHANGELOG, reformatted to remove line -/// breaks that will show up on GitHub. -String _lastChangelogSection() { - var scanner = StringScanner(File("CHANGELOG.md").readAsStringSync(), - sourceUrl: "CHANGELOG.md"); - - // Scans the remainder of the current line and returns it. This consumes the - // trailing newline but doesn't return it. - String scanLine() { - var buffer = StringBuffer(); - while (!scanner.isDone && scanner.peekChar() != $lf) { - buffer.writeCharCode(scanner.readChar()); - } - scanner.scanChar($lf); - return buffer.toString(); - } - - scanner.expect("## $pkgVersion\n"); - - var buffer = StringBuffer(); - while (!scanner.isDone && !scanner.matches("## ")) { - if (scanner.matches(_codeBlock)) { - do { - buffer.writeln(scanLine()); - } while (!scanner.matches(_codeBlock)); - buffer.writeln(scanLine()); - } else if (scanner.matches(RegExp(" *\n"))) { - buffer.writeln(); - buffer.writeln(scanLine()); - } else { - buffer.write(scanLine()); - buffer.writeCharCode($space); - } - } - - return buffer.toString().trim(); -} - -/// Uploads 32- and 64-bit Linux executable packages to the GitHub release for -/// the current version. -/// -/// This uploads the archives generated by [pkgStandaloneLinuxIa32] and -/// [pkgStandaloneLinuxX64]. It must be invoked after [pkgGithubRelease], but it -/// does not automatically invoke [pkgGithubRelease] to allow for different -/// operating systems' executables to be uploaded in separate steps. -/// -/// If this is run on Linux, the 64-bit executable is built as a native -/// executable. If it's run on any other operating system, it's built as a -/// snapshot instead, which is substantially slower. -@Task('Release Linux executables to GitHub.') -Future pkgGithubLinux({http.Client client}) async { - await pkgCompileSnapshot(); - await withClient(client, (client) async { - await Future.wait([ - pkgStandaloneLinuxIa32(client: client), - pkgStandaloneLinuxX64(client: client) - ]); - await _uploadExecutables("linux", client: client); - }); -} - -/// Uploads 32- and 64-bit Mac OS executable packages to the GitHub release for -/// the current version. -/// -/// This uploads the archives generated by [pkgStandaloneMacOsIa32] and -/// [pkgStandaloneMacOsX64]. It must be invoked after [pkgGithubRelease], but it -/// does not automatically invoke [pkgGithubRelease] to allow for different -/// operating systems' executables to be uploaded in separate steps. -/// -/// If this is run on Mac OS, the 64-bit executable is built as a native -/// executable. If it's run on any other operating system, it's built as a -/// snapshot instead, which is substantially slower. -@Task('Release Mac OS executables to GitHub.') -Future pkgGithubMacOs({http.Client client}) async { - await pkgCompileSnapshot(); - await withClient(client, (client) async { - await Future.wait([ - pkgStandaloneMacOsIa32(client: client), - pkgStandaloneMacOsX64(client: client) - ]); - await _uploadExecutables("macos", client: client); - }); -} - -/// Uploads 32- and 64-bit Windows executable packages to the GitHub release for -/// the current version. -/// -/// This uploads the archives generated by [pkgStandaloneWindowsIa32] and -/// [pkgStandaloneWindowsX64]. It must be invoked after [pkgGithubRelease], but it -/// does not automatically invoke [pkgGithubRelease] to allow for different -/// operating systems' executables to be uploaded in separate steps. -/// -/// If this is run on Windows, the 64-bit executable is built as a native -/// executable. If it's run on any other operating system, it's built as a -/// snapshot instead, which is substantially slower. -@Task('Release Windows executables to GitHub.') -Future pkgGithubWindows({http.Client client}) async { - await pkgCompileSnapshot(); - await withClient(client, (client) async { - await Future.wait([ - pkgStandaloneWindowsIa32(client: client), - pkgStandaloneWindowsX64(client: client) - ]); - await _uploadExecutables("windows", client: client); - }); -} - -/// Creates a GitHub release for [pkgVersion] of this package and uploads 32- -/// and 64-bit Linux, Mac OS, and Windows executables for this package. -/// -/// This creates the GitHub release using [pkgGithubRelease] and uploads -/// archives generated by [pkgStandaloneLinuxIa32], [pkgStandaloneLinuxX64], -/// [pkgStandaloneMacOsIa32], [pkgStandaloneMacOsX64], -/// [pkgStandaloneWindowsIa32], and [pkgStandaloneWindowsX64]. -/// -/// Note that this will only build native executables for the operating system -/// it's invoked on. All other operating systems' executables will be built as -/// snapshots, which are substantially slower. To build all snapshots as native -/// executables, invoke the individual [pkgGithubLinux], [pkgGithubMacOs], and -/// [pkgGithubWindows] tasks on their respective oeprating systems. -@Task('Create a GitHub release with all executables.') -Future pkgGithubAll({http.Client client}) async { - await withClient(client, (client) async { - await pkgGithubRelease(client: client); - await Future.wait([ - pkgGithubLinux(client: client), - pkgGithubMacOs(client: client), - pkgGithubWindows(client: client) - ]); - }); -} - -/// Upload the 32- and 64-bit executables to the current GitHub release -Future _uploadExecutables(String os, {http.Client client}) async { - return withClient(client, (client) async { - var response = await client.get( - url("https://api.github.com/repos/$pkgGithubRepo/tags/$pkgVersion"), - headers: {"authorization": _authorization}); - - var uploadUrl = json - .decode(response.body)["upload_url"] - // Remove the URL template. - .replaceFirst(RegExp(r"\{[^}]+\}$"), ""); - - await Future.wait(["ia32", "x64"].map((architecture) async { - var format = os == "windows" ? "zip" : "tar.gz"; - var package = "$pkgStandaloneName-$pkgVersion-$os-$architecture.$format"; - var response = await client.post("$uploadUrl?name=$package", - headers: { - "content-type": - os == "windows" ? "application/zip" : "application/gzip", - "authorization": _authorization - }, - body: File(p.join("build", package)).readAsBytesSync()); - - if (response.statusCode != 201) { - fail("${response.statusCode} error uploading $package:\n" - "${response.body}"); - } else { - log("Uploaded $package."); - } - })); - }); -} diff --git a/lib/src/github.dart b/lib/src/github.dart new file mode 100644 index 0000000..67c661a --- /dev/null +++ b/lib/src/github.dart @@ -0,0 +1,269 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:charcode/charcode.dart'; +import 'package:grinder/grinder.dart'; +import 'package:path/path.dart' as p; +import 'package:string_scanner/string_scanner.dart'; + +import 'info.dart'; +import 'standalone.dart'; +import 'utils.dart'; + +/// The GitHub repository slug (for example, `username/repo`) to which to upload +/// package releases. +/// +/// By default, this determines the repo from Git's `origin` remote, failing +/// that, from the pubspec's `homepage` field. If neither of those is a valid +/// Git URL, this must be set explicitly. +String get githubRepo { + _githubRepo ??= _repoFromOrigin() ?? _repoFromPubspec(); + if (_githubRepo != null) return _githubRepo; + + fail("pkg.githubRepo must be set to deploy to GitHub."); +} + +set githubRepo(String value) => _githubRepo = value; +String _githubRepo; + +/// Returns the GitHub repo name from the Git configuration's +/// `remote.origin.url` field. +String _repoFromOrigin() { + try { + var result = Process.runSync("git", ["config", "remote.origin.url"]); + if (result.exitCode != 0) return null; + var origin = (result.stdout as String).trim(); + return origin.startsWith("http") ? _parseHttp(origin) : _parseGit(origin); + } on IOException { + return null; + } +} + +/// Returns the GitHub repo name from the pubspec's `homepage` field. +String _repoFromPubspec() => + pubspec.homepage == null ? null : _parseHttp(pubspec.homepage); + +/// Parses a GitHub repo name from an SSH reference or a `git://` URL. +/// +/// Returns `null` if it couldn't be parsed. +String _parseGit(String url) { + var match = RegExp(r"^(git@github\.com:|git://github\.com/)" + r"(?[^/]+/[^/]+?)(\.git)?$") + .firstMatch(url); + return match == null ? null : match.namedGroup('repo'); +} + +/// Parses a GitHub repo name from an HTTP or HTTPS URL. +/// +/// Returns `null` if it couldn't be parsed. +String _parseHttp(String url) { + var match = RegExp(r"^https?://github\.com/([^/]+/[^/]+)").firstMatch(url); + return match == null ? null : match[1]; +} + +/// The GitHub username to use when creating releases and making other changes. +/// +/// By default, this comes from the `GITHUB_USER` environment variable. +String get githubUser { + _githubUser ??= Platform.environment["GITHUB_USER"]; + if (_githubUser != null) return _githubUser; + + fail("pkg.githubUser must be set to deploy to GitHub."); +} + +set githubUser(String value) => _githubUser = value; +String _githubUser; + +/// The GitHub password or authentication token to use when creating releases +/// and making other changes. +/// +/// **Do not check this in directly.** This should only come from secure +/// sources. +/// +/// This can be either the username itself, a [personal access token][], or an +/// OAuth token. +/// +/// [personal access token]: https://github.com/settings/tokens +/// +/// By default, this comes from the `GITHUB_PASSWORD` environment variable if it +/// exists, or `GITHUB_TOKEN` otherwise. +String get githubPassword { + _githubPassword ??= Platform.environment["GITHUB_PASSWORD"] ?? + Platform.environment["GITHUB_TOKEN"]; + if (_githubPassword != null) return _githubPassword; + + fail("pkg.githubPassword must be set to deploy to GitHub."); +} + +set githubPassword(String value) => _githubPassword = value; +String _githubPassword; + +/// Returns the HTTP basic authentication Authorization header from the +/// environment. +String get _authorization => + "Basic ${base64.encode(utf8.encode("$githubUser:$githubPassword"))}"; + +/// The Markdown-formatted release notes to use for the GitHub release. +/// +/// By default, this looks for a `CHANGELOG.md` file at the root of the +/// repository and uses the portion between the first and second level-2 `##` +/// headers. It throws a [FormatException] if the CHANGELOG doesn't begin with +/// `##` followed by [version]. +String get githubReleaseNotes { + if (_githubReleaseNotes != null) return _githubReleaseNotes; + if (!File("CHANGELOG.md").existsSync()) return null; + + _githubReleaseNotes = _lastChangelogSection() + + "\n\n" + "See the [full changelog](https://github.com/$githubRepo/blob/" + "master/CHANGELOG.md#${version.toString().replaceAll(".", "")}) " + "for changes in earlier releases."; + return _githubReleaseNotes; +} + +set githubReleaseNotes(String value) => _githubReleaseNotes = value; +String _githubReleaseNotes; + +/// Creates a GitHub release for [version] of this package. +/// +/// This creates the release on the [githubRepo] repository, using +/// [humanName] as the release name and [githubReleaseNotes] as the +/// release notes. +Future _release() async { + var response = await client.post( + url("https://api.github.com/repos/$githubRepo/releases"), + headers: { + "content-type": "application/json", + "authorization": _authorization + }, + body: jsonEncode({ + "tag_name": version.toString(), + "name": "$humanName $version", + "prerelease": version.isPreRelease, + if (githubReleaseNotes != null) "body": githubReleaseNotes + })); + + if (response.statusCode != 201) { + fail("${response.statusCode} error creating release:\n${response.body}"); + } else { + log("Released $humanName $version to GitHub."); + } +} + +/// A regular expression that matches a Markdown code block. +final _codeBlock = RegExp(" *```"); + +/// Returns the most recent section in the CHANGELOG, reformatted to remove line +/// breaks that will show up on GitHub. +String _lastChangelogSection() { + var scanner = StringScanner(File("CHANGELOG.md").readAsStringSync(), + sourceUrl: "CHANGELOG.md"); + + // Scans the remainder of the current line and returns it. This consumes the + // trailing newline but doesn't return it. + String scanLine() { + var buffer = StringBuffer(); + while (!scanner.isDone && scanner.peekChar() != $lf) { + buffer.writeCharCode(scanner.readChar()); + } + scanner.scanChar($lf); + return buffer.toString(); + } + + scanner.expect("## $version\n"); + + var buffer = StringBuffer(); + while (!scanner.isDone && !scanner.matches("## ")) { + if (scanner.matches(_codeBlock)) { + do { + buffer.writeln(scanLine()); + } while (!scanner.matches(_codeBlock)); + buffer.writeln(scanLine()); + } else if (scanner.matches(RegExp(" *\n"))) { + buffer.writeln(); + buffer.writeln(scanLine()); + } else { + buffer.write(scanLine()); + buffer.writeCharCode($space); + } + } + + return buffer.toString().trim(); +} + +/// Whether [addGithubTasks] has been called yet. +var _addedGithubTasks = false; + +/// Enables tasks for releasing the package on GitHub releases. +void addGithubTasks() { + if (_addedGithubTasks) return; + _addedGithubTasks = true; + + addStandaloneTasks(); + + addTask(GrinderTask('pkg-github-release', + taskFunction: _release, + description: 'Create a GitHub release, without executables.')); + + for (var os in ["linux", "macos", "windows"]) { + addTask(GrinderTask('pkg-github-$os', + taskFunction: () => _uploadExecutables(os), + description: 'Release ${humanOSName(os)} executables to GitHub.', + depends: ['pkg-standalone-$os-ia32', 'pkg-standalone-$os-x64'])); + } + + addTask(GrinderTask('pkg-github-all', + description: 'Create a GitHub release with all executables.', + depends: [ + 'pkg-github-release', + 'pkg-github-linux', + 'pkg-github-macos', + 'pkg-github-windows' + ])); +} + +/// Upload the 32- and 64-bit executables to the current GitHub release +Future _uploadExecutables(String os) async { + var response = await client.get( + url("https://api.github.com/repos/$githubRepo/tags/$version"), + headers: {"authorization": _authorization}); + + var uploadUrl = json + .decode(response.body)["upload_url"] + // Remove the URL template. + .replaceFirst(RegExp(r"\{[^}]+\}$"), ""); + + await Future.wait(["ia32", "x64"].map((architecture) async { + var format = os == "windows" ? "zip" : "tar.gz"; + var package = "$standaloneName-$version-$os-$architecture.$format"; + var response = await client.post("$uploadUrl?name=$package", + headers: { + "content-type": + os == "windows" ? "application/zip" : "application/gzip", + "authorization": _authorization + }, + body: File(p.join("build", package)).readAsBytesSync()); + + if (response.statusCode != 201) { + fail("${response.statusCode} error uploading $package:\n" + "${response.body}"); + } else { + log("Uploaded $package."); + } + })); +} diff --git a/lib/src/info.dart b/lib/src/info.dart index 6f5594b..7943cca 100644 --- a/lib/src/info.dart +++ b/lib/src/info.dart @@ -27,34 +27,34 @@ final _rawPubspec = loadYaml(File('pubspec.yaml').readAsStringSync(), sourceUrl: 'pubspec.yaml'); /// The name of the package, as specified in the pubspec. -final pkgDartName = pubspec.name; +final dartName = pubspec.name; /// The package's version, as specified in the pubspec. -final pkgVersion = pubspec.version; +final version = pubspec.version; /// The default name of the package on package managers other than pub. /// /// Pub requires that a package name be a valid Dart identifier, but other /// package managers do not and users may wish to choose a different name for -/// them. This defaults to [pkgDartName]. -String get pkgName => _pkgName ?? pkgDartName; -set pkgName(String value) => _pkgName = value; -String _pkgName; +/// them. This defaults to [dartName]. +String get name => _name ?? dartName; +set name(String value) => _name = value; +String _name; /// The human-friendly name of the package. /// /// This is used in places where the package name is only meant to be read by -/// humans, not used as a filename or identifier. It defaults to [pkgName]. -String get pkgHumanName => _pkgHumanName ?? pkgName; -set pkgHumanName(String value) => _pkgHumanName = value; -String _pkgHumanName; +/// humans, not used as a filename or identifier. It defaults to [name]. +String get humanName => _humanName ?? name; +set humanName(String value) => _humanName = value; +String _humanName; /// A mutable map from executable names to those executables' paths in `bin/`. /// /// This defaults to a map derived from the pubspec's `executables` field. It /// may be modified, but the values must be paths to executable files in the /// package. -Map pkgExecutables = () { +Map executables = () { var executables = _rawPubspec['executables'] as Map; return { diff --git a/lib/src/standalone.dart b/lib/src/standalone.dart new file mode 100644 index 0000000..e321c2f --- /dev/null +++ b/lib/src/standalone.dart @@ -0,0 +1,203 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:archive/archive.dart'; +import 'package:grinder/grinder.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +import 'info.dart'; +import 'template.dart'; +import 'utils.dart'; + +/// Whether we're using a 64-bit Dart SDK. +bool get _is64Bit => Platform.version.contains("x64"); + +/// The name of the standalone package. +/// +/// This defaults to [name]. +String get standaloneName => _standaloneName ?? name; +set standaloneName(String value) => _standaloneName = value; +String _standaloneName; + +/// For each executable entrypoint in [executables], builds a script snapshot +/// to `build/${executable}.snapshot`. +void _compileSnapshot() { + ensureBuild(); + + for (var entrypoint in entrypoints) { + Dart.run(entrypoint, + vmArgs: ['--snapshot=build/${p.basename(entrypoint)}.snapshot']); + } +} + +/// For each executable entrypoint in [executables], builds a native ("AOT") +/// executable to `build/${executable}.native`. +void _compileNative() { + ensureBuild(); + + if (!File(p.join(sdkDir.path, 'bin/dart2aot')).existsSync()) { + fail( + "Your SDK doesn't have dart2aot. This probably means that you're using " + "a 32-bit SDK, which doesn't support native compilation."); + } + + for (var entrypoint in entrypoints) { + run(p.join(sdkDir.path, 'bin/dart2aot'), arguments: [ + entrypoint, + '-Dversion=$version', + 'build/${p.basename(entrypoint)}.native' + ]); + } +} + +/// Whehter [addStandaloneTasks] has been called yet. +var _addedStandaloneTasks = false; + +/// Enables tasks for building standalone Dart VM packages. +void addStandaloneTasks() { + if (_addedStandaloneTasks) return; + _addedStandaloneTasks = true; + + addTask(GrinderTask('pkg-compile-snapshot', + taskFunction: _compileSnapshot, + description: 'Build Dart script snapshot(s).')); + + addTask(GrinderTask('pkg-compile-native', + taskFunction: _compileNative, + description: 'Build Dart native executable(s).')); + + for (var os in ["linux", "macos", "windows"]) { + addTask(GrinderTask('pkg-standalone-$os-ia32', + taskFunction: () => _buildPackage(os, x64: false), + description: + 'Build a standalone 32-bit package for ${humanOSName(os)}.', + depends: ['pkg-compile-snapshot'])); + + addTask(GrinderTask('pkg-standalone-$os-x64', + taskFunction: () => _buildPackage(os, x64: true), + description: + 'Build a standalone 64-bit package for ${humanOSName(os)}.', + depends: _useNative(os, x64: true) + ? ['pkg-compile-native'] + : ['pkg-compile-snapshot'])); + } + + addTask(GrinderTask('pkg-standalone-all', + description: 'Build all standalone packages.', + depends: [ + for (var os in ["linux", "macos", "windows"]) + for (var arch in ["ia32", "x64"]) "pkg-standalone-$os-$arch" + ])); +} + +/// Returns whether to use the natively-compiled executable for the given [os] +/// and architecture combination. +/// +/// We can only use the native executable on the current operating system *and* +/// on 64-bit machines, because currently Dart doesn't support cross-compilation +/// (dart-lang/sdk#28617) and only 64-bit Dart SDKs ship with `dart2aot`. +bool _useNative(String os, {@required bool x64}) => + os == Platform.operatingSystem && x64 == _is64Bit; + +/// Builds a Sass package for the given [os] and architecture. +Future _buildPackage(String os, {@required bool x64}) async { + var archive = Archive() + ..addFile(fileFromBytes( + "$standaloneName/src/dart${os == 'windows' ? '.exe' : ''}", + await _dartExecutable(os, x64: x64), + executable: true)) + ..addFile(file( + "$standaloneName/src/DART_LICENSE", p.join(sdkDir.path, 'LICENSE'))); + + if (File("LICENSE").existsSync()) { + archive.addFile(file("$standaloneName/src/LICENSE", "LICENSE")); + } + + for (var entrypoint in entrypoints) { + var basename = p.basename(entrypoint); + archive.addFile(file( + "$standaloneName/src/$basename.snapshot", + _useNative(os, x64: x64) + ? "build/$basename.native" + : "build/$basename.snapshot")); + } + + // Do this separately from adding entrypoints because multiple executables may + // have the same entrypoint. + executables.forEach((name, path) { + archive.addFile(fileFromString( + "$standaloneName/$name${os == 'windows' ? '.bat' : ''}", + renderTemplate( + "standalone/executable.${os == 'windows' ? 'bat' : 'sh'}", { + "name": standaloneName, + "version": _useNative(os, x64: x64) ? null : version.toString(), + "executable": p.basename(path) + }), + executable: true)); + }); + + var prefix = 'build/$standaloneName-$version-$os-${_arch(x64)}'; + if (os == 'windows') { + var output = "$prefix.zip"; + log("Creating $output..."); + File(output).writeAsBytesSync(ZipEncoder().encode(archive)); + } else { + var output = "$prefix.tar.gz"; + log("Creating $output..."); + File(output) + .writeAsBytesSync(GZipEncoder().encode(TarEncoder().encode(archive))); + } +} + +/// Returns the binary contents of the `dart` or `dartaotruntime` exectuable for +/// the given [os] and architecture. +Future> _dartExecutable(String os, {@required bool x64}) async { + // If we're building for the same SDK we're using, load its executable from + // disk rather than downloading it fresh. + if (_useNative(os, x64: x64)) { + return File(p.join( + sdkDir.path, "bin/dartaotruntime${os == 'windows' ? '.exe' : ''}")) + .readAsBytesSync(); + } else if (isTesting) { + // Don't actually download full SDKs in test mode, just return a dummy + // executable. + return utf8.encode("Dart $os ${_arch(x64)}"); + } + + // TODO(nweiz): Compile a single executable that embeds the Dart VM and the + // snapshot when dart-lang/sdk#27596 is fixed. + var channel = isDevSdk ? "dev" : "stable"; + var url = "https://storage.googleapis.com/dart-archive/channels/$channel/" + "release/$dartVersion/sdk/dartsdk-$os-${_arch(x64)}-release.zip"; + log("Downloading $url..."); + var response = await client.get(Uri.parse(url)); + if (response.statusCode ~/ 100 != 2) { + fail("Failed to download package: ${response.statusCode} " + "${response.reasonPhrase}."); + } + + var filename = "/bin/dart${os == 'windows' ? '.exe' : ''}"; + return ZipDecoder() + .decodeBytes(response.bodyBytes) + .firstWhere((file) => file.name.endsWith(filename)) + .content as List; +} + +/// Returns the architecture name for the given boolean. +String _arch(bool x64) => x64 ? "x64" : "ia32"; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index ed364c1..8982964 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -22,7 +22,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'info.dart'; /// The set of entrypoint for executables defined by this package. -Set get entrypoints => pkgExecutables.values.toSet(); +Set get entrypoints => executables.values.toSet(); /// The version of the current Dart executable. final Version dartVersion = Version.parse(Platform.version.split(" ").first); @@ -33,6 +33,11 @@ bool get isDevSdk => dartVersion.isPreRelease; /// Returns whether tasks are being run in a test environment. bool get isTesting => Platform.environment["_CLI_PKG_TESTING"] == "true"; +/// A shared client to use across all HTTP requests. +/// +/// This will automatically be cleaned up when the process exits. +final client = http.Client(); + /// Ensure that the `build/` directory exists. void ensureBuild() { Directory('build').createSync(recursive: true); @@ -74,16 +79,16 @@ Uri url(String url) { scheme: parsedHost.scheme, host: parsedHost.host, port: parsedHost.port); } -/// Passes [client] to [callback] and returns the result. -/// -/// If [client] is `null`, creates a client just for the duration of [callback]. -Future withClient( - http.Client client, Future callback(http.Client client)) async { - var createdClient = client == null; - client ??= http.Client(); - try { - return await callback(client); - } finally { - if (createdClient) await client.close(); +/// Returns the human-friendly name for the given [os] string. +String humanOSName(String os) { + switch (os) { + case "linux": + return "Linux"; + case "macos": + return "Mac OS"; + case "windows": + return "Windows"; + default: + throw ArgumentError("Unknown OS $os."); } } diff --git a/lib/standalone.dart b/lib/standalone.dart deleted file mode 100644 index c8179e9..0000000 --- a/lib/standalone.dart +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'src/info.dart'; -import 'src/template.dart'; -import 'src/utils.dart'; - -import 'package:archive/archive.dart'; -import 'package:grinder/grinder.dart'; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; - -/// Whether we're using a 64-bit Dart SDK. -bool get _is64Bit => Platform.version.contains("x64"); - -/// The name of the standalone package. -/// -/// This defaults to [pkgName]. -String get pkgStandaloneName => _pkgStandaloneName ?? pkgName; -set pkgStandaloneName(String value) => _pkgStandaloneName = value; -String _pkgStandaloneName; - -/// For each executable entrypoint in [pkgExecutables], builds a script snapshot -/// to `build/${executable}.snapshot`. -@Task('Build Dart script snapshot(s).') -void pkgCompileSnapshot() { - ensureBuild(); - - for (var entrypoint in entrypoints) { - Dart.run(entrypoint, - vmArgs: ['--snapshot=build/${p.basename(entrypoint)}.snapshot']); - } -} - -/// For each executable entrypoint in [pkgExecutables], builds a native ("AOT") -/// executable to `build/${executable}.native`. -@Task('Build Dart native executable(s).') -void pkgCompileNative() { - ensureBuild(); - - if (!File(p.join(sdkDir.path, 'bin/dart2aot')).existsSync()) { - fail( - "Your SDK doesn't have dart2aot. This probably means that you're using " - "a 32-bit SDK, which doesn't support native compilation."); - } - - for (var entrypoint in entrypoints) { - run(p.join(sdkDir.path, 'bin/dart2aot'), arguments: [ - entrypoint, - '-Dversion=$pkgVersion', - 'build/${p.basename(entrypoint)}.native' - ]); - } -} - -/// Builds a standalone 32-bit package for Linux to -/// `build/$pkgStandaloneName-$pkgVersion-linux-ia32.tar.gz`. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// version. -@Depends(pkgCompileSnapshot) -@Task('Build a standalone 32-bit package for Linux.') -Future pkgStandaloneLinuxIa32({http.Client client}) => - _buildPackage("linux", x64: false, client: client); - -/// Builds a standalone 64-bit package for Linux to -/// `build/$pkgStandaloneName-$pkgVersion-linux-x64.tar.gz`. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// version. -@Task('Build a standalone 64-bit package for Linux.') -Future pkgStandaloneLinuxX64({http.Client client}) async { - _compile("linux", x64: true); - await _buildPackage("linux", x64: true, client: client); -} - -/// Builds a standalone 32-bit package for MacOs to -/// `build/$pkgStandaloneName-$pkgVersion-macos-ia32.tar.gz`. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// version. -@Depends(pkgCompileSnapshot) -@Task('Build a standalone 32-bit package for Mac OS.') -Future pkgStandaloneMacOsIa32({http.Client client}) => - _buildPackage("macos", x64: false, client: client); - -/// Builds a standalone 64-bit package for MacOs to -/// `build/$pkgStandaloneName-$pkgVersion-macos-x64.tar.gz`. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// version. -@Task('Build a standalone 64-bit package for Mac OS.') -Future pkgStandaloneMacOsX64({http.Client client}) async { - _compile("macos", x64: true); - await _buildPackage("macos", x64: true, client: client); -} - -/// Builds a standalone 32-bit package for Windows to -/// `build/$pkgStandaloneName-$pkgVersion-windows-ia32.zip`. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// version. -@Depends(pkgCompileSnapshot) -@Task('Build a standalone 32-bit package for Windows.') -Future pkgStandaloneWindowsIa32({http.Client client}) => - _buildPackage("windows", x64: false, client: client); - -/// Builds a standalone 64-bit package for Windows to -/// `build/$pkgStandaloneName-$pkgVersion-windows-x64.zip`. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// version. -@Task('Build a standalone 64-bit package for Windows.') -Future pkgStandaloneWindowsX64({http.Client client}) async { - _compile("windows", x64: true); - await _buildPackage("windows", x64: true, client: client); -} - -/// Builds standalone 32- and 64-bit packages for all operating systems. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// versions. -@Task('Build all standalone packages.') -@Depends(pkgCompileSnapshot, pkgCompileNative) -Future pkgStandaloneAll({http.Client client}) { - return withClient(client, (client) { - return Future.wait([ - _buildPackage("linux", x64: false, client: client), - _buildPackage("linux", x64: true, client: client), - _buildPackage("macos", x64: false, client: client), - _buildPackage("macos", x64: true, client: client), - _buildPackage("windows", x64: false, client: client), - _buildPackage("windows", x64: true, client: client), - ]); - }); -} - -/// Compiles a native executable if it's supported for the OS/architecture -/// combination, and compiles a script snapshot otherwise. -void _compile(String os, {@required bool x64}) { - if (_useNative(os, x64: x64)) { - pkgCompileNative(); - } else { - pkgCompileSnapshot(); - } -} - -/// Returns whether to use the natively-compiled executable for the given [os] -/// and architecture combination. -/// -/// We can only use the native executable on the current operating system *and* -/// on 64-bit machines, because currently Dart doesn't support cross-compilation -/// (dart-lang/sdk#28617) and only 64-bit Dart SDKs ship with `dart2aot`. -bool _useNative(String os, {@required bool x64}) => - os == Platform.operatingSystem && x64 == _is64Bit; - -/// Builds a Sass package for the given [os] and architecture. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// version. -Future _buildPackage(String os, - {@required bool x64, http.Client client}) async { - var archive = Archive() - ..addFile(fileFromBytes( - "$pkgStandaloneName/src/dart${os == 'windows' ? '.exe' : ''}", - await _dartExecutable(os, x64: x64, client: client), - executable: true)) - ..addFile(file( - "$pkgStandaloneName/src/DART_LICENSE", p.join(sdkDir.path, 'LICENSE'))); - - if (File("LICENSE").existsSync()) { - archive.addFile(file("$pkgStandaloneName/src/LICENSE", "LICENSE")); - } - - for (var entrypoint in entrypoints) { - var basename = p.basename(entrypoint); - archive.addFile(file( - "$pkgStandaloneName/src/$basename.snapshot", - _useNative(os, x64: x64) - ? "build/$basename.native" - : "build/$basename.snapshot")); - } - - // Do this separately from adding entrypoints because multiple executables may - // have the same entrypoint. - pkgExecutables.forEach((name, path) { - archive.addFile(fileFromString( - "$pkgStandaloneName/$name${os == 'windows' ? '.bat' : ''}", - renderTemplate( - "standalone/executable.${os == 'windows' ? 'bat' : 'sh'}", { - "name": pkgStandaloneName, - "version": _useNative(os, x64: x64) ? null : pkgVersion.toString(), - "executable": p.basename(path) - }), - executable: true)); - }); - - var prefix = 'build/$pkgStandaloneName-$pkgVersion-$os-${_arch(x64)}'; - if (os == 'windows') { - var output = "$prefix.zip"; - log("Creating $output..."); - File(output).writeAsBytesSync(ZipEncoder().encode(archive)); - } else { - var output = "$prefix.tar.gz"; - log("Creating $output..."); - File(output) - .writeAsBytesSync(GZipEncoder().encode(TarEncoder().encode(archive))); - } -} - -/// Returns the binary contents of the `dart` or `dartaotruntime` exectuable for -/// the given [os] and architecture. -/// -/// If [client] is passed, it's used to download the corresponding Dart SDK -/// version. -Future> _dartExecutable(String os, - {@required bool x64, http.Client client}) async { - // If we're building for the same SDK we're using, load its executable from - // disk rather than downloading it fresh. - if (_useNative(os, x64: x64)) { - return File(p.join( - sdkDir.path, "bin/dartaotruntime${os == 'windows' ? '.exe' : ''}")) - .readAsBytesSync(); - } else if (isTesting) { - // Don't actually download full SDKs in test mode, just return a dummy - // executable. - return utf8.encode("Dart $os ${_arch(x64)}"); - } - - // TODO(nweiz): Compile a single executable that embeds the Dart VM and the - // snapshot when dart-lang/sdk#27596 is fixed. - var channel = isDevSdk ? "dev" : "stable"; - var url = "https://storage.googleapis.com/dart-archive/channels/$channel/" - "release/$dartVersion/sdk/dartsdk-$os-${_arch(x64)}-release.zip"; - log("Downloading $url..."); - var response = - await withClient(client, (client) => client.get(Uri.parse(url))); - if (response.statusCode ~/ 100 != 2) { - fail("Failed to download package: ${response.statusCode} " - "${response.reasonPhrase}."); - } - - var filename = "/bin/dart${os == 'windows' ? '.exe' : ''}"; - return ZipDecoder() - .decodeBytes(response.bodyBytes) - .firstWhere((file) => file.name.endsWith(filename)) - .content as List; -} - -/// Returns the architecture name for the given boolean. -String _arch(bool x64) => x64 ? "x64" : "ia32"; diff --git a/test/descriptor.dart b/test/descriptor.dart index 632edc6..25e033e 100644 --- a/test/descriptor.dart +++ b/test/descriptor.dart @@ -70,7 +70,7 @@ DirectoryDescriptor package( dir("tool", [ file("grind.dart", """ - import 'package:cli_pkg/cli_pkg.dart'; + import 'package:cli_pkg/cli_pkg.dart' as pkg; import 'package:grinder/grinder.dart'; $grindDotDart diff --git a/test/github_test.dart b/test/github_test.dart index 9fc7243..ffc328c 100644 --- a/test/github_test.dart +++ b/test/github_test.dart @@ -41,13 +41,13 @@ void main() { group("repo name", () { group("throws an error", () { test("if it's not set anywhere", () async { - await d.package("my_app", pubspec, _exportGithub()).create(); + await d.package("my_app", pubspec, _enableGithub()).create(); var process = await grind(["pkg-github-release"]); expect( process.stdout, emitsThrough( - contains("pkgGithubRepo must be set to deploy to GitHub."))); + contains("pkg.githubRepo must be set to deploy to GitHub."))); await process.shouldExit(1); }); @@ -56,19 +56,19 @@ void main() { .package( "my_app", {...pubspec, "homepage": "http://my-cool-package.pkg"}, - _exportGithub()) + _enableGithub()) .create(); var process = await grind(["pkg-github-release"]); expect( process.stdout, emitsThrough( - contains("pkgGithubRepo must be set to deploy to GitHub."))); + contains("pkg.githubRepo must be set to deploy to GitHub."))); await process.shouldExit(1); }); test("if it's not parsable from the Git config", () async { - await d.package("my_app", pubspec, _exportGithub()).create(); + await d.package("my_app", pubspec, _enableGithub()).create(); await git(["init"]); await git(["remote", "add", "origin", "git://random-url.com/repo"]); @@ -76,7 +76,7 @@ void main() { expect( process.stdout, emitsThrough( - contains("pkgGithubRepo must be set to deploy to GitHub."))); + contains("pkg.githubRepo must be set to deploy to GitHub."))); await process.shouldExit(1); }); }); @@ -85,7 +85,7 @@ void main() { Future assertParses(String homepage, String repo) async { await d .package( - "my_app", {...pubspec, "homepage": homepage}, _exportGithub()) + "my_app", {...pubspec, "homepage": homepage}, _enableGithub()) .create(); await _release(repo); } @@ -112,7 +112,7 @@ void main() { .package( "my_app", {...pubspec, "homepage": "http://github.com/google/wrong"}, - _exportGithub()) + _enableGithub()) .create(); await git(["init"]); @@ -123,7 +123,7 @@ void main() { group("parses from the Git origin", () { Future assertParses(String origin, String repo) async { - await d.package("my_app", pubspec, _exportGithub()).create(); + await d.package("my_app", pubspec, _enableGithub()).create(); await git(["init"]); await git(["remote", "add", "origin", origin]); await _release(repo); @@ -162,13 +162,11 @@ void main() { test("prefers an explicit repo URL to Git origin", () async { await d.package("my_app", pubspec, """ - import 'package:cli_pkg/github.dart'; - export 'package:cli_pkg/github.dart'; - void main(List args) { - pkgGithubUser = "usr"; - pkgGithubPassword = "pwd"; - pkgGithubRepo = "google/right"; + pkg.githubUser = "usr"; + pkg.githubPassword = "pwd"; + pkg.githubRepo = "google/right"; + pkg.addGithubTasks(); grind(args); } """).create(); @@ -189,27 +187,27 @@ void main() { test("throws an error if it's not set anywhere", () async { await d - .package("my_app", pubspecWithHomepage, _exportGithub(user: false)) + .package("my_app", pubspecWithHomepage, _enableGithub(user: false)) .create(); var process = await grind(["pkg-github-release"]); expect( process.stdout, emitsThrough( - contains("pkgGithubUser must be set to deploy to GitHub."))); + contains("pkg.githubUser must be set to deploy to GitHub."))); await process.shouldExit(1); }); test("parses from the GITHUB_USER environment variable", () async { await d - .package("my_app", pubspecWithHomepage, _exportGithub(user: false)) + .package("my_app", pubspecWithHomepage, _enableGithub(user: false)) .create(); await assertUsername("fblthp", environment: {"GITHUB_USER": "fblthp"}); }); test("prefers an explicit username to the GITHUB_USER environment variable", () async { - await d.package("my_app", pubspecWithHomepage, _exportGithub()).create(); + await d.package("my_app", pubspecWithHomepage, _enableGithub()).create(); await assertUsername("usr", environment: {"GITHUB_USER": "wrong"}); }); }); @@ -225,21 +223,21 @@ void main() { test("throws an error if it's not set anywhere", () async { await d .package( - "my_app", pubspecWithHomepage, _exportGithub(password: false)) + "my_app", pubspecWithHomepage, _enableGithub(password: false)) .create(); var process = await grind(["pkg-github-release"]); expect( process.stdout, emitsThrough( - contains("pkgGithubPassword must be set to deploy to GitHub."))); + contains("pkg.githubPassword must be set to deploy to GitHub."))); await process.shouldExit(1); }); test("parses from the GITHUB_TOKEN environment variable", () async { await d .package( - "my_app", pubspecWithHomepage, _exportGithub(password: false)) + "my_app", pubspecWithHomepage, _enableGithub(password: false)) .create(); await assertPassword("secret", environment: {"GITHUB_TOKEN": "secret"}); }); @@ -248,7 +246,7 @@ void main() { () async { await d .package( - "my_app", pubspecWithHomepage, _exportGithub(password: false)) + "my_app", pubspecWithHomepage, _enableGithub(password: false)) .create(); await assertPassword("right", environment: {"GITHUB_PASSWORD": "right", "GITHUB_TOKEN": "wrong"}); @@ -257,7 +255,7 @@ void main() { test( "prefers an explicit username to the GITHUB_PASSWORD environment variable", () async { - await d.package("my_app", pubspecWithHomepage, _exportGithub()).create(); + await d.package("my_app", pubspecWithHomepage, _enableGithub()).create(); await assertPassword("pwd", environment: {"GITHUB_PASSWORD": "wrong"}); }); }); @@ -272,13 +270,13 @@ void main() { Future assertReleaseNotesFromChangelog( String changelog, matcher) async { - await d.package("my_app", pubspecWithHomepage, _exportGithub()).create(); + await d.package("my_app", pubspecWithHomepage, _enableGithub()).create(); await d.file("my_app/CHANGELOG.md", changelog).create(); await assertReleaseNotes(matcher); } test("isn't set in the request if it's not set anywhere", () async { - await d.package("my_app", pubspecWithHomepage, _exportGithub()).create(); + await d.package("my_app", pubspecWithHomepage, _enableGithub()).create(); await _release("my_org/my_app", verify: (request) async { expect( @@ -368,13 +366,11 @@ void main() { test("prefers explicit release notes to the CHANGELOG", () async { await d.package("my_app", pubspecWithHomepage, """ - import 'package:cli_pkg/github.dart'; - export 'package:cli_pkg/github.dart'; - void main(List args) { - pkgGithubUser = "usr"; - pkgGithubPassword = "pwd"; - pkgGithubReleaseNotes = "right"; + pkg.githubUser = "usr"; + pkg.githubPassword = "pwd"; + pkg.githubReleaseNotes = "right"; + pkg.addGithubTasks(); grind(args); } """).create(); @@ -383,17 +379,17 @@ void main() { }); }); - test("pkg-github-mac-os uploads standalone Mac OS archives", () async { - await d.package("my_app", pubspecWithHomepage, _exportGithub()).create(); + test("pkg-github-macos uploads standalone Mac OS archives", () async { + await d.package("my_app", pubspecWithHomepage, _enableGithub()).create(); await _release("my_org/my_app"); var server = await _assertUploadsPackage("macos"); - await (await grind(["pkg-github-mac-os"], server: server)).shouldExit(0); + await (await grind(["pkg-github-macos"], server: server)).shouldExit(0); await server.close(); }); test("pkg-github-linux uploads standalone Linux archives", () async { - await d.package("my_app", pubspecWithHomepage, _exportGithub()).create(); + await d.package("my_app", pubspecWithHomepage, _enableGithub()).create(); await _release("my_org/my_app"); var server = await _assertUploadsPackage("linux"); @@ -402,7 +398,7 @@ void main() { }); test("pkg-github-windows uploads standalone Windows archives", () async { - await d.package("my_app", pubspecWithHomepage, _exportGithub()).create(); + await d.package("my_app", pubspecWithHomepage, _enableGithub()).create(); await _release("my_org/my_app"); var server = await _assertUploadsPackage("windows"); @@ -411,22 +407,20 @@ void main() { }); } -/// The contents of a `grind.dart` file that just exports -/// `package:cli_pkg/github.dart`. +/// The contents of a `grind.dart` file that just enables GitHub tasks. /// /// If [user] and [password] are `true`, this sets default values for -/// `pkgGithubUser` and `pkgGithubPassword`, respectively. -String _exportGithub({bool user = true, bool password = true}) { +/// `pkg.githubUser` and `pkg.githubPassword`, respectively. +String _enableGithub({bool user = true, bool password = true}) { var buffer = StringBuffer(""" - import 'package:cli_pkg/github.dart'; - export 'package:cli_pkg/github.dart'; - void main(List args) { """); - if (user) buffer.writeln('pkgGithubUser = "usr";'); - if (password) buffer.writeln('pkgGithubPassword = "pwd";'); - buffer.writeln("grind(args);\n}"); + if (user) buffer.writeln('pkg.githubUser = "usr";'); + if (password) buffer.writeln('pkg.githubPassword = "pwd";'); + buffer.writeln("pkg.addGithubTasks();"); + buffer.writeln("grind(args);"); + buffer.writeln("}"); return buffer.toString(); } diff --git a/test/standalone_test.dart b/test/standalone_test.dart index bc94b80..5337b07 100644 --- a/test/standalone_test.dart +++ b/test/standalone_test.dart @@ -34,12 +34,12 @@ final _archiveSuffix = _target + (Platform.isWindows ? ".zip" : ".tar.gz"); /// The extension for executable scripts on the current platform. final _dotBat = Platform.isWindows ? ".bat" : ""; -/// The contents of a `grind.dart` file that just exports -/// `package:cli_pkg/standalone.dart`. -final _exportStandalone = """ - export 'package:cli_pkg/standalone.dart'; - - void main(List args) => grind(args); +/// The contents of a `grind.dart` file that just enables standalone tasks. +final _enableStandalone = """ + void main(List args) { + pkg.addStandaloneTasks(); + grind(args); + } """; void main() { @@ -50,8 +50,8 @@ void main() { "executables": {"foo": "foo"} }; - test("default to pkgDartName", () async { - await d.package("my_app", pubspec, _exportStandalone).create(); + test("default to pkg.dartName", () async { + await d.package("my_app", pubspec, _enableStandalone).create(); await (await grind(["pkg-standalone-$_target"])).shouldExit(0); @@ -59,12 +59,11 @@ void main() { [d.dir("my_app")]).validate(); }); - test("prefer pkgName to pkgDartName", () async { + test("prefer pkg.name to pkg.dartName", () async { await d.package("my_app", pubspec, """ - export 'package:cli_pkg/standalone.dart'; - void main(List args) { - pkgName = "my-app"; + pkg.name = "my-app"; + pkg.addStandaloneTasks(); grind(args); } """).create(); @@ -75,14 +74,12 @@ void main() { [d.dir("my-app")]).validate(); }); - test("prefer pkgStandaloneName to pkgName", () async { + test("prefer pkg.standaloneName to pkg.name", () async { await d.package("my_app", pubspec, """ - import 'package:cli_pkg/standalone.dart'; - export 'package:cli_pkg/standalone.dart'; - void main(List args) { - pkgName = "my-app"; - pkgStandaloneName = "my-sa-app"; + pkg.name = "my-app"; + pkg.standaloneName = "my-sa-app"; + pkg.addStandaloneTasks(); grind(args); } """).create(); @@ -102,7 +99,7 @@ void main() { }; test("default to the pubspec's executables", () async { - await d.package("my_app", pubspec, _exportStandalone).create(); + await d.package("my_app", pubspec, _enableStandalone).create(); await (await grind(["pkg-standalone-$_target"])).shouldExit(0); await d.archive("my_app/build/my_app-1.2.3-$_archiveSuffix", [ @@ -121,10 +118,9 @@ void main() { test("can be removed by the user", () async { await d.package("my_app", pubspec, """ - export 'package:cli_pkg/standalone.dart'; - void main(List args) { - pkgExecutables.remove("foo"); + pkg.executables.remove("foo"); + pkg.addStandaloneTasks(); grind(args); } """).create(); @@ -146,10 +142,9 @@ void main() { test("can be added by the user", () async { await d.package("my_app", pubspec, """ - export 'package:cli_pkg/standalone.dart'; - void main(List args) { - pkgExecutables["zip"] = "bin/foo.dart"; + pkg.executables["zip"] = "bin/foo.dart"; + pkg.addStandaloneTasks(); grind(args); } """).create(); @@ -174,7 +169,7 @@ void main() { // Normally each of these would be separate test cases, but running grinder // takes so long that we collapse them for efficiency. test("can be invoked", () async { - await d.package("my_app", pubspec, _exportStandalone).create(); + await d.package("my_app", pubspec, _enableStandalone).create(); await (await grind(["pkg-standalone-$_target"])).shouldExit(0); await extract("my_app/build/my_app-1.2.3-$_archiveSuffix", "out"); @@ -223,7 +218,7 @@ void main() { "version": "1.2.3", "executables": {"foo": "foo"} }, - _exportStandalone, + _enableStandalone, [d.file("LICENSE", "Please use my code")]) .create(); await (await grind(["pkg-standalone-$_target"])).shouldExit(0); @@ -245,7 +240,7 @@ void main() { "version": "1.2.3", "executables": {"foo": "foo"} }, - _exportStandalone) + _enableStandalone) .create()); d.Descriptor archive(String name, {bool windows = false}) => @@ -262,12 +257,12 @@ void main() { group("Mac OS", () { test("32-bit", () async { - await (await grind(["pkg-standalone-mac-os-ia32"])).shouldExit(0); + await (await grind(["pkg-standalone-macos-ia32"])).shouldExit(0); await archive("my_app/build/my_app-1.2.3-macos-ia32.tar.gz").validate(); }); test("64-bit", () async { - await (await grind(["pkg-standalone-mac-os-x64"])).shouldExit(0); + await (await grind(["pkg-standalone-macos-x64"])).shouldExit(0); await archive("my_app/build/my_app-1.2.3-macos-x64.tar.gz").validate(); }); });