diff --git a/generator/integration-tests/config/.gitignore b/generator/integration-tests/config/.gitignore new file mode 100644 index 000000000..83a54c29c --- /dev/null +++ b/generator/integration-tests/config/.gitignore @@ -0,0 +1,4 @@ +# start with an empty project, without a objectbox-model.json +objectbox-model.json +objectbox.* +testdata \ No newline at end of file diff --git a/generator/integration-tests/config/1.dart b/generator/integration-tests/config/1.dart new file mode 100644 index 000000000..e31017ef0 --- /dev/null +++ b/generator/integration-tests/config/1.dart @@ -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 env; + final jsonModel = readModelJson('lib/custom'); + final defs = getObjectBoxModel(); + final model = defs.model; + + setUp(() { + env = TestEnv(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(); + expect(box.count(), 0); + + final inserted = A(); + box.put(inserted); + expect(inserted.id, 1); + box.get(inserted.id!)!; + }); +} diff --git a/generator/integration-tests/config/lib/custom/.keep b/generator/integration-tests/config/lib/custom/.keep new file mode 100644 index 000000000..a0b7e79a7 --- /dev/null +++ b/generator/integration-tests/config/lib/custom/.keep @@ -0,0 +1 @@ +This file just exists so its folder is created by git. \ No newline at end of file diff --git a/generator/integration-tests/config/lib/lib.dart b/generator/integration-tests/config/lib/lib.dart new file mode 100644 index 000000000..89d8ec4e2 --- /dev/null +++ b/generator/integration-tests/config/lib/lib.dart @@ -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(); +} diff --git a/generator/integration-tests/config/pubspec.yaml b/generator/integration-tests/config/pubspec.yaml new file mode 100644 index 000000000..2b51f3927 --- /dev/null +++ b/generator/integration-tests/config/pubspec.yaml @@ -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 + diff --git a/generator/lib/objectbox_generator.dart b/generator/lib/objectbox_generator.dart index 8ae1ffdc4..b57c58fef 100644 --- a/generator/lib/objectbox_generator.dart +++ b/generator/lib/objectbox_generator.dart @@ -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); diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart index 5ca1b489c..083ab3d37 100644 --- a/generator/lib/src/code_builder.dart +++ b/generator/lib/src/code_builder.dart @@ -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 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 = >{}; - 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))!; } @@ -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"); @@ -69,8 +82,8 @@ class CodeBuilder extends Builder { List 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 = @@ -95,11 +108,42 @@ class CodeBuilder extends Builder { void updateCode(ModelInfo model, List 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); @@ -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); } diff --git a/generator/lib/src/config.dart b/generator/lib/src/config.dart new file mode 100644 index 000000000..1d081d3b4 --- /dev/null +++ b/generator/lib/src/config.dart @@ -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._(); + } +} diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index 509b039f4..237e3ea83 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -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: diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 56f124853..68124e9c3 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -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 diff --git a/objectbox/example/README.md b/objectbox/example/README.md index eeaf88e00..5dd9ac27a 100644 --- a/objectbox/example/README.md +++ b/objectbox/example/README.md @@ -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 ----------------