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
----------------