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

Add utility to collect headers from google/mediapipe #10

Merged
merged 9 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions .flutter-mediapipe-root
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Used to normalize the paths of commands.
// The contents of this file do not matter.
Piinks marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions packages/build_cmd/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
3 changes: 3 additions & 0 deletions packages/build_cmd/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0

- Initial version.
2 changes: 2 additions & 0 deletions packages/build_cmd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
A sample command-line application with an entrypoint in `bin/`, library code
craiglabenz marked this conversation as resolved.
Show resolved Hide resolved
in `lib/`, and example unit test in `test/`.
30 changes: 30 additions & 0 deletions packages/build_cmd/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.

Piinks marked this conversation as resolved.
Show resolved Hide resolved
include: package:lints/recommended.yaml

# Uncomment the following section to specify additional rules.

# linter:
# rules:
# - camel_case_types

# analyzer:
# exclude:
# - path/to/excluded/files/**

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints

# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
Piinks marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 10 additions & 0 deletions packages/build_cmd/bin/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:args/command_runner.dart';
craiglabenz marked this conversation as resolved.
Show resolved Hide resolved
import 'package:build/sync_headers.dart';

final runner = CommandRunner(
'build',
'Performs build operations for google/flutter-mediapipe that '
'depend on contents in this repository',
)..addCommand(SyncHeadersCommand());

void main(List<String> arguments) => runner.run(arguments);
103 changes: 103 additions & 0 deletions packages/build_cmd/lib/repo_finder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:path/path.dart' as path;
import 'package:io/ansi.dart';

/// Mixin to help [Command] subclasses locate both `google/mediapipe` and
/// the root of `google/flutter-mediapipe` (this repository).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intended to be run outside of the context of google/flutter-mediapipe? I wonder if we don't already know the root of this repository, why go looking for it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's so the command can be run from different locations within the repository. Without this, the dart ... command can only be run from whatever path this assumes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note on the Outdated flag - this code has not changed, only moved directories. The above conversation still applies as-is.

///
/// The primary methods are [findFlutterMediaPipeRoot] and [findMediaPipeRoot].
///
/// By default, the root for `google/flutter-mediapipe` is determined by the
/// firest ancestor directory which contains a `.flutter-mediapipe-root` file
/// (whose contents are irrelevant), and the root of `google/mediapipe` is
/// expected to be a sibling of that. However, the `--source` flag can overwrite
/// this expectation and specify an absolute path where to find `google/mediapipe`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what reason?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Developers may not have these repositories as siblings on their machines, and forcing that via this command feels presumptuous. WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note on the Outdated flag - this code has not changed, only moved directories. The above conversation still applies as-is.

///
/// Note that it is not possible to override the method of locating the root of
/// `google/flutter-mediapipe`.
mixin RepoFinderMixin on Command {
/// Name of the file which, when found, indicates the root of this repository.
static String sentinelFileName = '.flutter-mediapipe-root';

void addSourceOption(ArgParser argParser) {
argParser.addOption(
'source',
abbr: 's',
help: 'The location of google/mediapipe. Defaults to being '
'adjacent to google/flutter-mediapipe.',
);
}

/// Looks upward for the root of the `google/mediapipe` repository. This assumes
/// the `dart build` command is executed from within said repository. If it is
/// not executed from within, then this searching algorithm will reach the root
/// of the file system, log the error, and exit.
io.Directory findFlutterMediaPipeRoot() {
final placesChecked = <io.Directory>[];
io.Directory dir = io.Directory(path.current);
while (true) {
if (_isFlutterMediaPipeRoot(dir)) {
return dir;
}
placesChecked.add(dir);
dir = dir.parent;
if (dir.parent.path == dir.path) {
io.stderr.writeln(
wrapWith(
'Failed to find google/flutter-mediapipe root directory. '
'Did you execute this command from within the repository?\n'
'Looked in:',
[red],
),
);
io.stderr.writeln(
wrapWith(
placesChecked
.map<String>((dir) => ' - ${dir.absolute.path}')
.toList()
.join('\n'),
[red],
),
);
io.exit(1);
}
}
}

/// Finds the `google/mediapipe` checkout where artifacts built in this
/// repository should be sourced. By default, this command assumes the two
/// repositories are siblings on the file system, but the `--source` flag
/// allows for this assumption to be overridden.
io.Directory findMediaPipeRoot(
io.Directory flutterMediaPipeDir,
String? source,
) {
final mediaPipeDirectory = io.Directory(
source ??
path.joinAll([flutterMediaPipeDir.parent.absolute.path, 'mediapipe']),
);

if (!mediaPipeDirectory.existsSync()) {
io.stderr.writeln(
'Could not find ${mediaPipeDirectory.absolute.path}. '
'Folder does not exist.',
);
io.exit(1);
}
return mediaPipeDirectory;
}

/// Looks for the sentinel file of this repository's root directory. This allows
/// the `dart build` command to be run from various locations within the
/// `google/mediapipe` repository and still correctly set paths for all of its
/// operations.
bool _isFlutterMediaPipeRoot(io.Directory dir) {
return io.File(
path.joinAll(
[dir.absolute.path, sentinelFileName],
),
).existsSync();
}
}
176 changes: 176 additions & 0 deletions packages/build_cmd/lib/sync_headers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import 'dart:convert';
import 'dart:io' as io;
import 'package:args/command_runner.dart';
import 'package:build/repo_finder.dart';
import 'package:io/ansi.dart';
import 'package:path/path.dart' as path;
import 'package:process/process.dart';

/// Relative header paths (in both repositories)
final containers = 'mediapipe/tasks/c/components/containers';
final processors = 'mediapipe/tasks/c/components/processors';
final core = 'mediapipe/tasks/c/core';
final tc = 'mediapipe/tasks/c/text/text_classifier';

/// google/flutter-mediapipe package paths
final corePackage = 'packages/mediapipe-core/third_party';
final textPackage = 'packages/mediapipe-task-text/third_party';

/// First string is its relative location in both repositories,
/// Second string is its package location in `google/flutter-mediapipe`,
/// Third string is the file name
/// Fourth param is an optional function to modify the file
List<(String, String, String, Function(io.File)?)> headerPaths = [
(containers, corePackage, 'category.h', null),
(containers, corePackage, 'classification_result.h', null),
(core, corePackage, 'base_options.h', null),
(processors, corePackage, 'classifier_options.h', null),
(tc, textPackage, 'text_classifier.h', relativeIncludes),
];

/// Command to copy all necessary C header files into this repository.
///
/// Pulls a list of hard-coded header files out of various destinations within
/// the google/mediapipe repository and places them in the same paths within
/// this repository. The only major change between their orientation within the
/// source repository (google/mediapipe) and their orientation here is that
/// shared header files are placed in `mediapipe-core` here, and no such
/// distinction exists in the source. This also implies rewriting the import
/// paths within these files to match, as their old relative positioning is
/// disrupted by the move.
class SyncHeadersCommand extends Command with RepoFinderMixin {
@override
String description = 'Syncs header files to google/flutter-mediapipe';
@override
String name = 'headers';

SyncHeadersCommand() {
argParser.addFlag(
'overwrite',
abbr: 'o',
defaultsTo: true,
help: 'If true, will overwrite existing header files '
'at destination locations.',
);
addSourceOption(argParser);
}

@override
Future<void> run() async {
final io.Directory flutterMediaPipeDirectory = findFlutterMediaPipeRoot();
final io.Directory mediaPipeDirectory = findMediaPipeRoot(
flutterMediaPipeDirectory,
argResults!['source'],
);

final config = Options(
allowOverwrite: argResults!['overwrite'],
mediaPipeDir: mediaPipeDirectory,
flutterMediaPipeDir: flutterMediaPipeDirectory,
);

await copyHeaders(config);
}

/// Central method that does the work of actually moving the files into the
/// local repository and rewriting their relative import statements within
/// the files themselves.
Future<void> copyHeaders(Options config) async {
final mgr = LocalProcessManager();
for (final tup in headerPaths) {
final headerFile = io.File(path.joinAll(
[config.mediaPipeDir.absolute.path, tup.$1, tup.$3],
));
if (!headerFile.existsSync()) {
io.stderr.writeln(
'Expected to find ${headerFile.path}, but '
'file does not exist.',
);
io.exit(1);
}
final destinationPath = path.joinAll(
[config.flutterMediaPipeDir.absolute.path, tup.$2, tup.$1, tup.$3],
);
final destinationFile = io.File(destinationPath);
if (destinationFile.existsSync() && !config.allowOverwrite) {
io.stdout.writeAll(
[
'Warning: Not overwriting existing file at $destinationPath\n',
wrapWith('Skipping ${tup.$3}.\n', [cyan]),
],
);
continue;
}

// MediaPipe header files often come from deeply nested locations, and new
// header files could require new folders. Thus, create any missing folders.
ensureFolders(io.File(destinationPath));

final process = await mgr.start(['cp', headerFile.path, destinationPath]);
int processExitCode = await process.exitCode;
if (processExitCode != 0) {
final processStdErr = utf8.decoder.convert(
(await process.stderr.toList())
.fold<List<int>>([], (arr, el) => arr..addAll(el)));
io.stderr.write(wrapWith(processStdErr, [red]));

final processStdOut = utf8.decoder.convert(
(await process.stdout.toList())
.fold<List<int>>([], (arr, el) => arr..addAll(el)));
io.stderr.write(wrapWith(processStdOut, [red]));
io.exit(processExitCode);
} else {
io.stderr.writeln(wrapWith('Copied ${tup.$3}', [green]));
}

// Call the final modification function, if supplied
if (tup.$4 != null) {
tup.$4!.call(destinationFile);
}
}
}

/// Builds any missing folders between the file and the root of the repository
void ensureFolders(io.File file) {
io.Directory parent = file.parent;
List<io.Directory> dirsToCreate = [];
while (!parent.existsSync()) {
dirsToCreate.add(parent);
parent = parent.parent;
}
for (io.Directory dir in dirsToCreate.reversed) {
dir.createSync();
}
}
}

class Options {
const Options({
required this.allowOverwrite,
required this.mediaPipeDir,
required this.flutterMediaPipeDir,
});

final bool allowOverwrite;
final io.Directory mediaPipeDir;
final io.Directory flutterMediaPipeDir;
}

void relativeIncludes(io.File textClassifierHeader) {
assert(textClassifierHeader.path.endsWith('text_classifier.h'));
String contents = textClassifierHeader.readAsStringSync();

Map<String, String> rewrites = {
'mediapipe/tasks/c/components/containers/classification_result.h':
'../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/components/containers/classification_result.h',
'mediapipe/tasks/c/components/processors/classifier_options.h':
'../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/components/processors/classifier_options.h',
'mediapipe/tasks/c/core/base_options.h':
'../../../../../../../mediapipe-core/third_party/mediapipe/tasks/c/core/base_options.h',
};

for (final rewrite in rewrites.entries) {
contents = contents.replaceAll(rewrite.key, rewrite.value);
}
textClassifierHeader.writeAsStringSync(contents);
}
20 changes: 20 additions & 0 deletions packages/build_cmd/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: build
description: Performs build operations for google/flutter-mediapipe that depend
on contents in this repository.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
environment:
sdk: ^3.2.0-162.0.dev

# Add regular dependencies here.
dependencies:
args: ^2.4.2
io: ^1.0.4
logging: ^1.2.0
path: ^1.8.0
process: ^5.0.0

dev_dependencies:
ffigen: ^9.0.1
lints: ^2.1.0
test: ^1.24.0