Skip to content

Add an option to configure generator's output_dir #341

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

Merged
merged 1 commit into from
Nov 30, 2021
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 generator/integration-tests/config/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# start with an empty project, without a objectbox-model.json
objectbox-model.json
objectbox.*
testdata
48 changes: 48 additions & 0 deletions generator/integration-tests/config/1.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'dart:io';
import 'dart:typed_data';

import 'package:test/test.dart';

import 'lib/lib.dart';
import 'lib/custom/objectbox.g.dart';
import '../test_env.dart';
import '../common.dart';

void main() {
late TestEnv<A> env;
final jsonModel = readModelJson('lib/custom');
final defs = getObjectBoxModel();
final model = defs.model;

setUp(() {
env = TestEnv<A>(defs);
});

tearDown(() {
env.close();
});

commonModelTests(defs, jsonModel);

test('project must be generated properly', () {
expect(TestEnv.dir.existsSync(), true);
expect(File('lib/custom/objectbox.g.dart').existsSync(), true);
expect(File('lib/custom/objectbox-model.json').existsSync(), true);
});

// Very simple tests to ensure imports and generated code is correct.

test('types', () {
expect(property(model, 'A.text').type, OBXPropertyType.String);
});

test('db-ops-A', () {
final box = env.store.box<A>();
expect(box.count(), 0);

final inserted = A();
box.put(inserted);
expect(inserted.id, 1);
box.get(inserted.id!)!;
});
}
1 change: 1 addition & 0 deletions generator/integration-tests/config/lib/custom/.keep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file just exists so its folder is created by git.
13 changes: 13 additions & 0 deletions generator/integration-tests/config/lib/lib.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'dart:typed_data';

import 'package:objectbox/objectbox.dart';

import 'custom/objectbox.g.dart';

@Entity()
class A {
int? id;
String? text;

A();
}
29 changes: 29 additions & 0 deletions generator/integration-tests/config/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: objectbox_generator_test

environment:
sdk: ">=2.12.0 <3.0.0"

dependencies:
objectbox: any

dev_dependencies:
objectbox_generator: any
test: any
build_runner: any
build_test: any
io: any
path: any

dependency_overrides:
objectbox:
path: ../../../objectbox
objectbox_generator:
path: ../../


objectbox:
output_dir: custom
# output_dir:
# lib: custom
# test: other

9 changes: 7 additions & 2 deletions generator/lib/objectbox_generator.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/// This package provides code generation for ObjectBox in Dart/Flutter.

import 'package:build/build.dart';
import 'src/entity_resolver.dart';

import 'src/code_builder.dart';
import 'src/config.dart';
import 'src/entity_resolver.dart';

final _config = Config.readFromPubspec();

/// Finds all classes annotated with @Entity annotation and creates intermediate files for the generator.
Builder entityResolverFactory(BuilderOptions options) => EntityResolver();

/// Writes objectbox_model.dart and objectbox-model.json from the prepared .objectbox.info files found in the repo.
Builder codeGeneratorFactory(BuilderOptions options) => CodeBuilder();
Builder codeGeneratorFactory(BuilderOptions options) => CodeBuilder(_config);
72 changes: 58 additions & 14 deletions generator/lib/src/code_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,42 @@ import 'package:dart_style/dart_style.dart';
import 'package:source_gen/source_gen.dart';
import 'package:pubspec_parse/pubspec_parse.dart';

import 'config.dart';
import 'entity_resolver.dart';
import 'code_chunks.dart';

/// CodeBuilder collects all '.objectbox.info' files created by EntityResolver and generates objectbox-model.json and
/// objectbox_model.dart
class CodeBuilder extends Builder {
static final jsonFile = 'objectbox-model.json';
static final codeFile = 'objectbox.g.dart';
final Config _config;

@override
final buildExtensions = {r'$lib$': _outputs, r'$test$': _outputs};

// we can't write `jsonFile` as part of the output because we want it persisted, not removed before each generation
static final _outputs = [codeFile];
CodeBuilder(this._config);

String dir(BuildStep buildStep) => path.dirname(buildStep.inputId.path);
@override
late final buildExtensions = {
r'$lib$': [path.join(_config.outDirLib, _config.codeFile)],
r'$test$': [path.join(_config.outDirTest, _config.codeFile)]
};

String _dir(BuildStep buildStep) => path.dirname(buildStep.inputId.path);

String _outDir(BuildStep buildStep) {
var dir = _dir(buildStep);
if (dir.endsWith('test')) {
return dir + '/' + _config.outDirTest;
} else if (dir.endsWith('lib')) {
return dir + '/' + _config.outDirLib;
} else {
throw Exception('Unrecognized path being generated: $dir');
}
}

@override
FutureOr<void> build(BuildStep buildStep) async {
// build() will be called only twice, once for the `lib` directory and once for the `test` directory
// map from file name to a 'json' representation of entities
final files = <String, List<dynamic>>{};
final glob = Glob(dir(buildStep) + '/**' + EntityResolver.suffix);
final glob = Glob(_dir(buildStep) + '/**' + EntityResolver.suffix);
await for (final input in buildStep.findAssets(glob)) {
files[input.path] = json.decode(await buildStep.readAsString(input))!;
}
Expand All @@ -55,7 +68,7 @@ class CodeBuilder extends Builder {

Pubspec? pubspec;
try {
final pubspecFile = File(path.join(dir(buildStep), '../pubspec.yaml'));
final pubspecFile = File(path.join(_dir(buildStep), '../pubspec.yaml'));
pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
} catch (e) {
log.info("Couldn't load pubspec.yaml: $e");
Expand All @@ -69,8 +82,8 @@ class CodeBuilder extends Builder {
List<ModelEntity> entities, BuildStep buildStep) async {
// load an existing model or initialize a new one
ModelInfo model;
final jsonId =
AssetId(buildStep.inputId.package, dir(buildStep) + '/' + jsonFile);
final jsonId = AssetId(
buildStep.inputId.package, _outDir(buildStep) + '/' + _config.jsonFile);
if (await buildStep.canRead(jsonId)) {
log.info('Using model: ${jsonId.path}');
model =
Expand All @@ -95,11 +108,42 @@ class CodeBuilder extends Builder {

void updateCode(ModelInfo model, List<String> infoFiles, BuildStep buildStep,
Pubspec? pubspec) async {
// If output directory is not package root directory,
// need to prefix imports with as many '../' to be relative from root.
final rootPath = _dir(buildStep);
final outPath = _outDir(buildStep);
final rootDir = Directory(rootPath).absolute;
var outDir = Directory(outPath).absolute;
var prefix = '';

if (!outDir.path.startsWith(rootDir.path)) {
throw InvalidGenerationSourceError(
'configured output_dir ${outDir.path} is not a '
'subdirectory of the source directory ${rootDir.path}');
}

while (outDir.path != rootDir.path) {
final parent = outDir.parent;
if (parent.path == outDir.path) {
log.warning(
'Failed to find package root from output directory, generated imports might be incorrect.');
prefix = '';
break; // Reached top-most directory, stop searching.
}
outDir = parent;
prefix += '../';
}
if (prefix.isNotEmpty) {
log.info(
'Output directory not in package root, adding prefix to imports: ' +
prefix);
}

// transform '/lib/path/entity.objectbox.info' to 'path/entity.dart'
final imports = infoFiles
.map((file) => file
.replaceFirst(EntityResolver.suffix, '.dart')
.replaceFirst(dir(buildStep) + '/', ''))
.replaceFirst(rootPath + '/', prefix))
.toList();

var code = CodeChunks.objectboxDart(model, imports, pubspec);
Expand All @@ -109,7 +153,7 @@ class CodeBuilder extends Builder {
} finally {
// Write the code even after a formatter error so it's easier to debug.
final codeId =
AssetId(buildStep.inputId.package, dir(buildStep) + '/' + codeFile);
AssetId(buildStep.inputId.package, outPath + '/' + _config.codeFile);
log.info('Generating code: ${codeId.path}');
await buildStep.writeAsString(codeId, code);
}
Expand Down
56 changes: 56 additions & 0 deletions generator/lib/src/config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'dart:io';

import 'package:yaml/yaml.dart';

const _pubspecFile = 'pubspec.yaml';
const _pubspecKey = 'objectbox';

/// Config reads and holds configuration for the code generator.
///
/// Expected format in pubspec.yaml:
/// ```
/// objectbox:
/// output_dir: custom
/// # Or optionally specify lib and test folder separately.
/// # output_dir:
/// # lib: custom
/// # test: other
/// ```
class Config {
final String jsonFile;
final String codeFile;
final String outDirLib;
final String outDirTest;

Config._(
{String? jsonFile,
String? codeFile,
String? outDirLib,
String? outDirTest})
: jsonFile = jsonFile ?? 'objectbox-model.json',
codeFile = codeFile ?? 'objectbox.g.dart',
outDirLib = outDirLib ?? '',
outDirTest = outDirTest ?? '';

factory Config.readFromPubspec() {
final file = File(_pubspecFile);
if (file.existsSync()) {
final yaml = loadYaml(file.readAsStringSync())[_pubspecKey] as YamlMap?;
if (yaml != null) {
late final String? outDirLib;
late final String? outDirTest;
final outDirYaml = yaml['output_dir'];

if (outDirYaml is YamlMap) {
outDirLib = outDirYaml['lib'];
outDirTest = outDirYaml['test'];
} else {
outDirLib = outDirTest = outDirYaml as String?;
}

return Config._(outDirLib: outDirLib, outDirTest: outDirTest);
}
}
return Config._();
}
}
1 change: 1 addition & 0 deletions generator/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies:
path: ^1.8.0
source_gen: ^1.0.0
pubspec_parse: ^1.0.0
yaml: ^3.0.0

# NOTE: remove before publishing
dependency_overrides:
Expand Down
2 changes: 2 additions & 0 deletions objectbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## latest

* Add an option to change code-generator's `output_dir` in `pubspec.yaml`. #341

## 1.3.0 (2021-11-22)

* Support annotating a single property with `@Unique(onConflict: ConflictStrategy.replace)` to
Expand Down
12 changes: 12 additions & 0 deletions objectbox/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ list (e.g. .gitignore), otherwise the build_runner will complain about it being
> annotations there. This is useful if you need a separate test DB. If you're just writing tests for your own code, you
> won't have any annotations in the `test` folder so no DB will be created there.

To customize the directory (relative to the package root) where the generated files are written,
add the following to your `pubspec.yaml`:
```
objectbox:
# Writes objectbox-model.json and objectbox.g.dart to lib/custom (and test/custom).
output_dir: custom
# Or optionally specify the lib and test output folder separately.
# output_dir:
# lib: custom
# test: other
```

Creating a store
----------------

Expand Down