Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation #1

Merged
merged 10 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
> Please include the following information:
>
> - Dart SDK version (`dart --version`):
> - dpx version (`dpx --version`):
36 changes: 36 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
branches:
- 'master'
- 'test_consume_*'
pull_request:
branches:
- '**'

jobs:
dart:
strategy:
fail-fast: false
matrix:
os: [ ubuntu, windows ]
sdk: [ 2.19.6, stable ]
name: Dart ${{ matrix.sdk }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
with:
sdk: ${{ matrix.sdk }}
- name: Install dependencies
run: dart pub get
- name: Analysis
run: dart analyze
- name: Validate dependencies
run: dart run dependency_validator
- name: Formatting
if: ${{ matrix.sdk == 'stable' && matrix.os == 'ubuntu' }}
run: dart format --output=none --set-exit-if-changed .
- name: Tests
run: dart test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.dart_tool/
pubspec.lock
128 changes: 127 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,127 @@
# dpx
# `dpx` - execute Dart package binaries

## Installation

Until this is published to pub, you'll have to install via Git:
```bash
dart pub global activate -sgit git@github.com:Workiva/dpx.git
```

For ease of use, [follow these instructions][dart run from path] to add the
system cache `bin` directory to your path so that you can run `dpx` directly.

## Usage

```bash
# Execute a command from <pkg> with the same name as <pkg>
dpx <pkg> [args...]

# Execute <cmd> from <pkg>.
# Use if there are multiple executables or if the executable name is different.
dpx --package=<pkg> <cmd> [args...]
```

## Command Running

Once the necessary package is installed, dpx will attempt to run the command.

First, it tries to run the command directly, assuming that it is available as an
executable in the PATH. This works for Dart packages that declare an
[executable in the pubspec][pubspec executable].

```yaml
# pubspec.yaml
name: webdev
executables:
webdev:
```

```bash
# Installs and runs `webdev` executable in PATH
dpx webdev
```

If that fails, dpx falls back to running the command with `dart pub global run`.
The expected format of a command run this way is `<pkg>:<cmd>`, where `<pkg>` is
the name of the Dart package and `<cmd>` is the name of the Dart file in `bin/`,
minus the `.dart` extension.

Dart lets you omit the `:<cmd>` portion if there's a file with the same name as
the package.

For other files, dpx lets you omit the `<pkg>` portion since it can be inferred.

```bash
dpx --package=build_runner :graph_inspector
```

## Package Sources

The first arg to `dpx` or the value of the `--package` option is referred to as
a `<package-spec>`, which supports several different formats to enable
installing from different sources and targeting specific versions.

```bash
# Install from pub with an optional version constraint.
# Syntax:
dpx <pkg>[@<version-constraint>] [args...]
# Example:
dpx webdev@^3.0.0 [args...]

# Install from custom pub server.
# Syntax:
dpx pub@<pub-server>:<pkg>[@<version-constraint] [args...]
# Example:
dpx pub@pub.workiva.org:workiva_nullsafety_migrator@^1.0.0

# Install from a github repo.
# Syntax:
dpx <git-url> [args...]
# Example:
dpx https://github.com/Workiva/dpx.git --help

# Shorthand for public github repos:
dpx github:<org>/<repo> [args...]

# Shorthand for private github repos:
dpx github+ssh:<org>/<repo> [args...]

# Optionally, all git-based package specs can specify:
# - <path> if the package is not in the root of the repo
# - <ref> to checkout a specific tag/branch/commit
# Syntax:
dpx <git-url>[#path:sub/dir,ref:v1.0.2] [args...]
# Examples:
dpx github:Workiva/dpx#ref:v0.0.0 --help
dpx github:Workiva/dpx#path:example/hello
dpx github:Workiva/dpx#path:example/hello,ref:v0.0.0
```

## Troubleshooting

If you encounter any issues, please run the command again with `--verbose`:

```bash
dpx --verbose ...
```

This will provide a lot more detail that might help identify an issue. If not,
please [open an issue][new issue] and include the verbose logs.

## Why the name?

It's like `npx`, but for **D**art.

**D**art **P**ackage e**X**ecute.

## Acknowledgements

`dpx` is inspired by the [`npx` package][npx package], which is now a part of
the [`npm` CLI][npx cli].

<!-- LINKS -->
[dart run from path]: https://dart.dev/tools/pub/cmd/pub-global#running-a-script-from-your-path
[new issue]: https://github.com/Workiva/dpx/issues/new
[npx cli]: https://docs.npmjs.com/cli/v8/commands/npx
[npx package]: https://www.npmjs.com/package/npx
[pubspec executable]: https://dart.dev/tools/pub/pubspec#executables
37 changes: 37 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# TODO

## Features / Functionality
- [ ] Support multiple `--package` options?
- [ ] Should `--yes` be supported? Risk vs convenience.
- [ ] Should dpx try to find and use a local install of the command first?

## Unit Tests
- [x] Package Spec parsing. Should cover:
- [x] `lib/src/package_spec_scanner.dart`
- [x] `lib/src/package_spec.dart`
- [x] `lib/src/pub_package_spec.dart`
- [x] `lib/src/git_package_spec.dart`
- [ ] `lib/src/args.dart`
- [ ] `lib/src/ensure_process_exit.dart`
- [ ] `lib/src/find_active_global_package.dart`
- [ ] `lib/src/find_reusable_package.dart`
- [ ] `lib/src/get_system_cache_path.dart`
- [ ] `lib/src/global_activate_package.dart`
- [ ] `lib/src/list_active_global_packages.dart`
- [ ] `lib/src/resolve_latest_git_ref.dart`

## End-to-end Tests
Should run the `dpx` executable to cover these use cases:

- [ ] Installing from:
- [ ] pub
- [ ] pub with version constraint
- [ ] custom pub (use the public pub, but explicitly specify the URL)
- [ ] github repo (https)
- [ ] github repo (ssh)
- [ ] github repo at ref
- [ ] github repo at subpath
- [ ] github repo at ref and subpath
- [ ] `--package` and `<cmd>` as first arg, in path
- [ ] `--package` and `<cmd>` as first arg, not in path
- [ ] `--package` and `<cmd>` as first arg, `:<cmd>` shorthand
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
13 changes: 13 additions & 0 deletions aviary.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 1

exclude:
- example/
- test/

raven_monitored_classes: null

raven_monitored_files: null

raven_monitored_functions: null

raven_monitored_keywords: null
113 changes: 113 additions & 0 deletions bin/dpx.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import 'dart:io';

import 'package:cli_util/cli_logging.dart';
import 'package:dpx/src/args.dart';
import 'package:dpx/src/ensure_process_exit.dart';
import 'package:dpx/src/exit_exception.dart';
import 'package:dpx/src/find_reusable_package.dart';
import 'package:dpx/src/global_activate_package.dart';
import 'package:dpx/src/package_spec.dart';
import 'package:dpx/src/package_spec_exception.dart';
import 'package:dpx/src/prompt.dart';
import 'package:io/io.dart';

void main(List<String> args) async {
final stopwatch = Stopwatch()..start();

try {
final dpxArgs = parseDpxArgs(args);
final logger = dpxArgs.verbose ? Logger.verbose() : Logger.standard();

PackageSpec spec;
try {
spec = PackageSpec.parse(dpxArgs.packageSpec);
} on PackageSpecException catch (error) {
throw ExitException(ExitCode.usage.code, '$error\n${usage()}');
}
logger.trace('Parsed package spec "${dpxArgs.packageSpec}" into $spec');

// Check if package is already installed at a suitable version/ref.
logger
.trace('Checking if suitable package is already installed globally...');
String? packageName;
var needsInstall = true;
final reusablePackage = await findReusablePackage(spec, logger: logger);
if (reusablePackage != null) {
packageName = reusablePackage;
needsInstall = false;
}

// Globally install package if needed.
if (needsInstall) {
logger
..stdout('Need to install the following packages:')
..stdout(spec.description);
if (!dpxArgs.autoInstall) {
stdout.write('Ok to proceed? (y/n) ');
final response = prompt('yn', 'n');
if (response != 'y') {
throw ExitException(ExitCode.usage.code, 'Canceled.');
}
}
final activatedPackageName =
await globalActivatePackage(spec, logger: logger);
packageName ??= activatedPackageName;
}

// Finalize the command to run.
String? command = dpxArgs.command;
if (command == null || command.startsWith(':')) {
if (packageName == null) {
throw ExitException(ExitCode.software.code,
'Could not infer package name to use as default command.');
}

if (command == null) {
// If command was not explicitly given, default to the package name.
command = packageName;
} else {
// If command starts with `:`, it's shorthand that omits the package name.
// Example: dpx --package=build_runner :graph_inspector --> dart pub global run build_runner:graph_inspector
command = '$packageName$command';
}
}

// Log how long DPX took before handing off to the actual command.
final dpxTime =
(stopwatch.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1);
stopwatch.stop();
logger.trace('Took ${dpxTime}s to start command.');

// First, try to run the command directly, assuming that it's in the PATH.
logger.trace('CMD: $command ${dpxArgs.commandArgs.join(' ')}');
try {
final process = await Process.start(
command,
dpxArgs.commandArgs,
mode: ProcessStartMode.inheritStdio,
);
ensureProcessExit(process);
} on ProcessException catch (e) {
if (e.message.contains('No such file')) {
// If command was not found in the PATH, fallback to `dart pub global run`
logger
..trace(
'Command not found in path, falling back to `dart pub global run`')
..trace(
'CMD: dart pub global run $command ${dpxArgs.commandArgs.join(' ')}');
final process = await Process.start(
'dart',
['pub', 'global', 'run', command, ...dpxArgs.commandArgs],
mode: ProcessStartMode.inheritStdio,
);
ensureProcessExit(process);
}
}
} on ExitException catch (error) {
print(error.message);
exit(error.exitCode);
} catch (error, stack) {
print('Unexpected uncaught exception:\n$error\n$stack');
exit(ExitCode.software.code);
}
}
Empty file added example/README.md
Empty file.
24 changes: 24 additions & 0 deletions example/dpx_hello/bin/dart.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
void main() async {
print(''' .....
..::::::::.
..::::::::::::::.
..::::::::::::::::::::.
--+++++++++++++++++++++++++-
:*::-+************************-
:**::::-+************************-
:***::::::-+************************-
:****::::::::-+************************-
:*****::::::::::-+************************:
.******::::::::::::-+**********************-
.+******::::::::::::::-+********************-
-*******::::::::::::::::-+******************-
=******::::::::::::::::::-+****************-
.=****::::::::::::::::::::-+**************-
.=**::::::::::::::::::::::-+************-
.=::::::::::::::::::::::::-+**********-
.:::::::::::::::::::::::::-+********-
.::::::::::::::::::::::::::::::::.
.::::::::::::::::::::::.
.::::::::::::::::::::.
.::::::::::::::::::.''');
}
12 changes: 12 additions & 0 deletions example/dpx_hello/bin/dpx_hello.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
void main() {
print('''8 888888888o. 8 888888888o `8.`8888. ,8'
8 8888 `^888. 8 8888 `88. `8.`8888. ,8'
8 8888 `88. 8 8888 `88 `8.`8888. ,8'
8 8888 `88 8 8888 ,88 `8.`8888.,8'
8 8888 88 8 8888. ,88' `8.`88888'
8 8888 88 8 888888888P' .88.`8888.
8 8888 ,88 8 8888 .8'`8.`8888.
8 8888 ,88' 8 8888 .8' `8.`8888.
8 8888 ,o88P' 8 8888 .8' `8.`8888.
8 888888888P' 8 8888 .8' `8.`8888. ''');
}
8 changes: 8 additions & 0 deletions example/dpx_hello/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: dpx_hello
version: 0.0.0

environment:
sdk: '>=2.19.0 <4.0.0'

executables:
dpx_hello:
Loading