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

Using extensions #355

Open
avioli opened this issue Oct 8, 2020 · 1 comment
Open

Using extensions #355

avioli opened this issue Oct 8, 2020 · 1 comment

Comments

@avioli
Copy link

avioli commented Oct 8, 2020

This is not an issue.

First off - thank you for the grinder tool. It is great find for me, since I found make didn't manage files with spaces and I have a bunch of them.

I've created a set of extensions to String, Glob, File, Directory, Iterable<File>, FileSet, and List<DateTime> that I found quite useful so I decided to share them here if anyone wants to use them or add them to the package or for their personal use.

Consider them public domain. They are created for grinder: "0.8.5". So they might not work for future visitors in case the package introduces breaking changes.

// grind_ext.dart
import 'dart:io';

import 'package:grinder/grinder.dart';
import 'package:glob/glob.dart';

// TODO: do we need more sophisticated glob detection?
final _globRegex = new RegExp(r'[*?{\[]');

extension FileSetStringExt on String {
  /// Returns a [File]
  File get asFile => File(this);

  /// Returns a [Directory]
  Directory get asDirectory => Directory(this);

  /// Converts this to a [File] and then to a [FileSet]
  ///
  /// If this includes [Glob] characters: *, ?, {, or [ it will first be
  /// converted to a [Glob] and that to a [FileSet].
  ///
  /// Note: [Glob] doesn't exclude files and dirs starting with a dot, while
  /// a recursive [FileSet] does.
  ///
  /// If you know this is a [Directory], then better use [allFiles].
  FileSet get asFileSet {
    if (this.contains(_globRegex)) {
      return Glob(this).asFileSet;
    }
    return this.asFile.asFileSet;
  }

  /// Converts this to a [Directory] and passes all params to its [allFiles]
  /// method
  FileSet allFiles({String pattern, bool recurse = true}) =>
      this.asDirectory.allFiles(pattern: pattern, recurse: recurse);

  /// Converts this to a [Directory] and passes all params to its [allDartFiles]
  /// method
  FileSet allDartFiles({bool recurse = true}) =>
      this.asDirectory.allDartFiles(recurse: recurse);
}

extension FileSetGlobExt on Glob {
  /// Runs [listSync] and collects all [File] objects into a [FileSet]
  FileSet get asFileSet =>
      listSync(followLinks: false).whereType<File>().asFileSet;
}

extension FileSetFileExt on File {
  /// Returns a [FileSet]
  FileSet get asFileSet => FileSet.fromFile(this);
}

extension FileSetDirExt on Directory {
  /// Returns a [FileSet], passing all params to [fromDir]
  FileSet fileSet({String pattern, bool recurse = true}) =>
      FileSet.fromDir(this, pattern: pattern, recurse: recurse);

  /// Returns the last modified date of the directory
  DateTime get lastModified => this.statSync().modified;

  /// Returns a [FileSet] using [fileSet]
  FileSet allFiles({String pattern, bool recurse = true}) =>
      this.fileSet(pattern: pattern, recurse: recurse);

  /// Returns a [FileSet] using [fileSet], scoped to Dart files only
  FileSet allDartFiles({bool recurse = true}) =>
      this.fileSet(pattern: '*.dart', recurse: recurse);
}

extension ListFileExt on Iterable<File> {
  /// Converts to a [FileSet]
  FileSet get asFileSet => FileSet.fromFile(this.first)..addFiles(this.skip(1));
}

extension ListStringExt on Iterable<String> {
  /// Converts to a list of [File] and then to a [FileSet]
  FileSet get asFileSet => map((el) => el.asFile).asFileSet;
}

extension OperationsExt on FileSet {
  /// Adds a [File] to the [files]
  void addFile(File file) {
    this.files.add(file);
  }

  /// Appends all [File] objects to the [files]
  void addFiles(Iterable<File> files) {
    this.files.addAll(files);
  }

  /// Adds a [String], [File], [Directory], [Glob] or another [FileSet],
  /// producing a new [FileSet]
  FileSet operator +(dynamic value) {
    if (value is FileSet) {
      // break-out first
    } else if (value is String) {
      value = FileSetStringExt(value).asFileSet;
    } else if (value is File) {
      value = FileSetFileExt(value).asFileSet;
    } else if (value is Directory) {
      value = FileSetDirExt(value).fileSet();
    } else if (value is Glob) {
      value = FileSetGlobExt(value).asFileSet;
    }
    if (value is FileSet) {
      return FileSet.fromFile(this.files.first)
        ..addFiles(this.files.skip(1))
        ..addFiles(value.files);
    }
    throw ArgumentError.value(value, 'value',
        'must be either a String, File, Directory, Glob or another FileSet');
  }
}

extension ListDateTimeExt on List<DateTime> {
  /// Returns tha latest [DateTime] in the list (or `null`)
  DateTime get latest {
    if (isEmpty) return null;
    final zero = DateTime.fromMillisecondsSinceEpoch(0);
    return reduce(
        (curr, el) => el != null && el.isAfter(curr ?? zero) ? el : curr);
  }
}
Open up these details if you want to see how I use them.
import 'package:grinder/grinder.dart';
import 'package:pubspec_yaml/pubspec_yaml.dart';

import './grind_ext.dart';

const projectName = 'Runner';
const iosOutDir = 'build/ios';

// NOTE: these are evaluated lazily - when read for the first time
final pubspec = 'pubspec.yaml'.asFile;
final version =
    pubspec.readAsStringSync().toPubspecYaml().version.valueOr(() => null);
final appFiles = pubspec.asFileSet + libDir.allDartFiles();
// --- iOS values
final iosFiles = appFiles +
    'ios/Podfile' +
    'ios/Flutter/{Debug,Release}.xcconfig'.asFileSet +
    'ios/$projectName'.allFiles() +
    'ios/$projectName.xcodeproj'.allFiles();
final iosFilesExists = iosFiles.exists;
final iosFilesLastModified = iosFiles.lastModified;
final iosIpa = '$iosOutDir/iphoneos/Runner.app'.asDirectory;
final iosXCArchiveName = '$projectName-$version';
final iosXCArchive = '$iosOutDir/$iosXCArchiveName.xcarchive'.asDirectory;

void main(List<String> args) => grind(args, verifyProjectRoot: true);

// /////////////////////////////////////////////////////////////////////////////

@DefaultTask()
void usage() => print('Run `grind --help` to list available tasks.');

// /////////////////////////////////////////////////////////////////////////////

@Task('Builds a release iOS archive.')
void buildIos() {
  if (version == null) fail('unable to read version in pubspec.yaml');
  if (!iosFilesExists) fail('missing iOS files');
  final hasArchive = iosXCArchive.existsSync();
  if (!hasArchive || iosFilesLastModified.isAfter(iosXCArchive.lastModified)) {
    // NOTE: we only want to depend on this if any of the iosFiles have changed!
    // TODO: can we generate ios/Flutter/Generated.xcconfig without building via flutter?
    flutterBuildIos();
    if (hasArchive) iosXCArchive.deleteSync(recursive: true);
    run('bash',
        arguments: ['build_release.sh'],
        workingDirectory: 'ios',
        runOptions:
            RunOptions(environment: {'XCARCHIVE_NAME': iosXCArchiveName}));
  } else {
    log('Archive is in ${iosXCArchive.path}');
  }
}

// /////////////////////////////////////////////////////////////////////////////

void flutterBuildIos() {
  if (version == null) fail('unable to read version in pubspec.yaml');
  if (!iosFilesExists) fail('missing iOS files');
  if (!iosIpa.existsSync() ||
      iosFilesLastModified.isAfter(iosIpa.lastModified)) {
    run('flutter', arguments: ['build', 'ios']);
  }
}

// /////////////////////////////////////////////////////////////////////////////

@Task('Deploys a release iOS archive.')
@Depends(buildIos)
void deployIos() {
  final exportOpts = 'ios/export_options.plist'.asFileSet;
  if (!exportOpts.exists)
    fail('Export options file is missing: ${exportOpts.files.first.path}');
  if (!iosXCArchive.existsSync())
    fail('Archive is missing: ${iosXCArchive.path}');
  run('bash',
      arguments: ['deploy_release.sh'],
      workingDirectory: 'ios',
      runOptions:
          RunOptions(environment: {'XCARCHIVE_NAME': iosXCArchiveName}));
  // TODO: add a file to track latest deployed version (hash?)
}

I couldn't find a way to put them in the codebase of the package, so no PR. Sorry.

@devoncarew
Copy link
Contributor

Thanks for the feedback! Happy to leave this open to make it easier for people to find this issue and the extensions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants