Automatically add std lib for macos, allow passing list to compiler-opts. (#193)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 319671a..40ae92a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 2.3.0
+- Added config key `compiler-opts-automatic -> macos -> include-c-standard-library`
+(default: true) to automatically find and add C standard library on macOS.
+- Allow passing list of string to config key `compiler-opts`.
+
 # 2.2.5
 - Added new command line flag `--compiler-opts` to the command line tool.
 
diff --git a/README.md b/README.md
index 5eb0103..e3faf97 100644
--- a/README.md
+++ b/README.md
@@ -140,7 +140,8 @@
     <td>
 
 ```yaml
-compiler-opts: '-I/usr/lib/llvm-9/include/'
+compiler-opts:
+  - '-I/usr/lib/llvm-9/include/'
 ```
 and/or via the command line -
 ```bash
@@ -149,6 +150,21 @@
 ```
   </td>
   </tr>
+    <tr>
+    <td>compiler-opts-automatic -> macos -> include-c-standard-library</td>
+    <td>Tries to automatically find and add C standard library path to
+    compiler-opts on macos.
+    <b>Default: true</b>
+    </td>
+    <td>
+
+```yaml
+compiler-opts-automatic:
+  macos:
+    include-c-standard-library: false
+```
+  </td>
+  </tr>
   <tr>
     <td>functions<br>structs<br>enums<br>unnamed-enums<br>macros<br>globals</td>
     <td>Filters for declarations.<br><b>Default: all are included</b></td>
diff --git a/example/libclang-example/pubspec.yaml b/example/libclang-example/pubspec.yaml
index fe0f30b..a7cbf4a 100644
--- a/example/libclang-example/pubspec.yaml
+++ b/example/libclang-example/pubspec.yaml
@@ -29,7 +29,9 @@
       - '**CXString.h'
       - '**Index.h'
 
-  compiler-opts: '-Ithird_party/libclang/include -IC:\Progra~1\LLVM\include -I/usr/local/opt/llvm/include/ -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/ -Wno-nullability-completeness'
+  compiler-opts:
+    - '-Ithird_party/libclang/include'
+    - '-Wno-nullability-completeness'
   functions:
     include:
       - 'clang_.*' # Can be a regexp, '.' matches any character.
diff --git a/lib/src/config_provider/config.dart b/lib/src/config_provider/config.dart
index 434137e..673daea 100644
--- a/lib/src/config_provider/config.dart
+++ b/lib/src/config_provider/config.dart
@@ -136,41 +136,14 @@
     }
   }
 
-  /// Checks if there are nested [key] in [map].
-  bool _checkKeyInYaml(List<String> key, YamlMap map) {
-    dynamic last = map;
-    for (final k in key) {
-      if (last is YamlMap) {
-        if (!last.containsKey(k)) return false;
-        last = last[k];
-      } else {
-        return false;
-      }
-    }
-    return last != null;
-  }
-
-  /// Extracts value of nested [key] from [map].
-  dynamic _getKeyValueFromYaml(List<String> key, YamlMap map) {
-    if (_checkKeyInYaml(key, map)) {
-      dynamic last = map;
-      for (final k in key) {
-        last = last[k];
-      }
-      return last;
-    }
-
-    return null;
-  }
-
   /// Validates Yaml according to given specs.
   bool _checkConfigs(YamlMap map, Map<List<String>, Specification> specs) {
     var _result = true;
     for (final key in specs.keys) {
       final spec = specs[key];
-      if (_checkKeyInYaml(key, map)) {
+      if (checkKeyInYaml(key, map)) {
         _result =
-            _result && spec!.validator(key, _getKeyValueFromYaml(key, map));
+            _result && spec!.validator(key, getKeyValueFromYaml(key, map));
       } else if (spec!.requirement == Requirement.yes) {
         _logger.severe("Key '$key' is required.");
         _result = false;
@@ -195,8 +168,8 @@
   void _extract(YamlMap map, Map<List<String>, Specification> specs) {
     for (final key in specs.keys) {
       final spec = specs[key];
-      if (_checkKeyInYaml(key, map)) {
-        spec!.extractedResult(spec.extractor(_getKeyValueFromYaml(key, map)));
+      if (checkKeyInYaml(key, map)) {
+        spec!.extractedResult(spec.extractor(getKeyValueFromYaml(key, map)));
       } else {
         spec!.extractedResult(spec.defaultValue?.call());
       }
@@ -235,6 +208,15 @@
         extractedResult: (dynamic result) =>
             _compilerOpts = result as List<String>,
       ),
+      [strings.compilerOptsAuto]: Specification<CompilerOptsAuto>(
+          requirement: Requirement.no,
+          validator: compilerOptsAutoValidator,
+          extractor: compilerOptsAutoExtractor,
+          defaultValue: () => CompilerOptsAuto(),
+          extractedResult: (dynamic result) {
+            _compilerOpts
+                .addAll((result as CompilerOptsAuto).extractCompilerOpts());
+          }),
       [strings.functions]: Specification<Declaration>(
         requirement: Requirement.no,
         validator: declarationConfigValidator,
diff --git a/lib/src/config_provider/config_types.dart b/lib/src/config_provider/config_types.dart
index 35d24f0..0240c19 100644
--- a/lib/src/config_provider/config_types.dart
+++ b/lib/src/config_provider/config_types.dart
@@ -3,9 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Contains all the neccesary classes required by config.
+import 'dart:io';
 
 import 'package:quiver/pattern.dart' as quiver;
 
+import 'path_finder.dart';
+
 class CommentType {
   CommentStyle style;
   CommentLength length;
@@ -319,3 +322,20 @@
     return member;
   }
 }
+
+/// Handles config for automatically added compiler options.
+class CompilerOptsAuto {
+  final bool macIncludeStdLib;
+
+  CompilerOptsAuto({bool? macIncludeStdLib})
+      : macIncludeStdLib = macIncludeStdLib ?? true;
+
+  /// Extracts compiler options based on OS and config.
+  List<String> extractCompilerOpts() {
+    if (Platform.isMacOS && macIncludeStdLib) {
+      return getCStandardLibraryHeadersForMac();
+    }
+
+    return [];
+  }
+}
diff --git a/lib/src/config_provider/path_finder.dart b/lib/src/config_provider/path_finder.dart
new file mode 100644
index 0000000..3f6a597
--- /dev/null
+++ b/lib/src/config_provider/path_finder.dart
@@ -0,0 +1,63 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utils for finding header paths on system.
+
+import 'dart:io';
+
+import 'package:logging/logging.dart';
+import 'package:path/path.dart' as p;
+
+final _logger = Logger('ffigen.config_provider.path_finder');
+
+/// This will return include path from either LLVM, XCode or CommandLineTools.
+List<String> getCStandardLibraryHeadersForMac() {
+  final includePaths = <String>[];
+
+  /// Add system headers.
+  const systemHeaders =
+      '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include';
+  if (Directory(systemHeaders).existsSync()) {
+    _logger.fine('Added $systemHeaders to compiler-opts.');
+    includePaths.add('-I' + systemHeaders);
+  }
+
+  /// Find headers from XCode or LLVM installed via brew.
+  const brewLlvmPath = '/usr/local/opt/llvm/lib/clang';
+  const xcodeClangPath =
+      '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/';
+  const searchPaths = [brewLlvmPath, xcodeClangPath];
+  for (final searchPath in searchPaths) {
+    if (!Directory(searchPath).existsSync()) continue;
+
+    final result = Process.runSync('ls', [searchPath]);
+    final stdout = result.stdout as String;
+    if (stdout != '') {
+      final versions = stdout.split('\n').where((s) => s != '');
+      for (final version in versions) {
+        final path = p.join(searchPath, version, 'include');
+        if (Directory(path).existsSync()) {
+          _logger.fine('Added stdlib path: $path to compiler-opts.');
+          includePaths.add('-I' + path);
+          return includePaths;
+        }
+      }
+    }
+  }
+
+  /// If CommandLineTools are installed use those headers.
+  const cmdLineToolHeaders =
+      '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Kernel.framework/Headers/';
+  if (Directory(cmdLineToolHeaders).existsSync()) {
+    _logger.fine('Added stdlib path: $cmdLineToolHeaders to compiler-opts.');
+    includePaths.add('-I' + cmdLineToolHeaders);
+    return includePaths;
+  }
+
+  // Warnings for missing headers are printed by libclang while parsing.
+  _logger.fine('Couldn\'t find stdlib headers in default locations.');
+  _logger.fine('Paths searched: ${[cmdLineToolHeaders, ...searchPaths]}');
+
+  return [];
+}
diff --git a/lib/src/config_provider/spec_utils.dart b/lib/src/config_provider/spec_utils.dart
index 692f921..ce6aae5 100644
--- a/lib/src/config_provider/spec_utils.dart
+++ b/lib/src/config_provider/spec_utils.dart
@@ -38,6 +38,33 @@
   return true;
 }
 
+/// Checks if there are nested [key] in [map].
+bool checkKeyInYaml(List<String> key, YamlMap map) {
+  dynamic last = map;
+  for (final k in key) {
+    if (last is YamlMap) {
+      if (!last.containsKey(k)) return false;
+      last = last[k];
+    } else {
+      return false;
+    }
+  }
+  return last != null;
+}
+
+/// Extracts value of nested [key] from [map].
+dynamic getKeyValueFromYaml(List<String> key, YamlMap map) {
+  if (checkKeyInYaml(key, map)) {
+    dynamic last = map;
+    for (final k in key) {
+      last = last[k];
+    }
+    return last;
+  }
+
+  return null;
+}
+
 bool booleanExtractor(dynamic value) => value as bool;
 
 bool booleanValidator(List<String> name, dynamic value) =>
@@ -120,11 +147,69 @@
   return list;
 }
 
-List<String> compilerOptsExtractor(dynamic value) =>
-    compilerOptsToList(value as String);
+List<String> compilerOptsExtractor(dynamic value) {
+  if (value is String) {
+    return compilerOptsToList(value);
+  }
 
-bool compilerOptsValidator(List<String> name, dynamic value) =>
-    checkType<String>(name, value);
+  final list = <String>[];
+  for (final el in (value as YamlList)) {
+    if (el is String) {
+      list.addAll(compilerOptsToList(el));
+    }
+  }
+  return list;
+}
+
+bool compilerOptsValidator(List<String> name, dynamic value) {
+  if (value is String || value is YamlList) {
+    return true;
+  } else {
+    _logger.severe('Expected $name to be a String or List of String.');
+    return false;
+  }
+}
+
+CompilerOptsAuto compilerOptsAutoExtractor(dynamic value) {
+  return CompilerOptsAuto(
+    macIncludeStdLib: getKeyValueFromYaml(
+      [strings.macos, strings.includeCStdLib],
+      value as YamlMap,
+    ) as bool?,
+  );
+}
+
+bool compilerOptsAutoValidator(List<String> name, dynamic value) {
+  var _result = true;
+
+  if (!checkType<YamlMap>(name, value)) {
+    return false;
+  }
+
+  for (final oskey in (value as YamlMap).keys) {
+    if (oskey == strings.macos) {
+      if (!checkType<YamlMap>([...name, oskey as String], value[oskey])) {
+        return false;
+      }
+
+      for (final inckey in (value[oskey] as YamlMap).keys) {
+        if (inckey == strings.includeCStdLib) {
+          if (!checkType<bool>(
+              [...name, oskey, inckey as String], value[oskey][inckey])) {
+            _result = false;
+          }
+        } else {
+          _logger.severe("Unknown key '$inckey' in '$name -> $oskey.");
+          _result = false;
+        }
+      }
+    } else {
+      _logger.severe("Unknown key '$oskey' in '$name'.");
+      _result = false;
+    }
+  }
+  return _result;
+}
 
 Headers headersExtractor(dynamic yamlConfig) {
   final entryPoints = <String>[];
@@ -169,7 +254,7 @@
     return false;
   }
   if (!(value as YamlMap).containsKey(strings.entryPoints)) {
-    _logger.severe("Expected '$name -> ${strings.entryPoints}' to be a Map.");
+    _logger.severe("Required '$name -> ${strings.entryPoints}'.");
     return false;
   } else {
     for (final key in value.keys) {
diff --git a/lib/src/header_parser/parser.dart b/lib/src/header_parser/parser.dart
index a8473af..ff28679 100644
--- a/lib/src/header_parser/parser.dart
+++ b/lib/src/header_parser/parser.dart
@@ -62,6 +62,7 @@
     config.compilerOpts.add(strings.fparseAllComments);
   }
 
+  _logger.fine('CompilerOpts used: ${config.compilerOpts}');
   clangCmdArgs = createDynamicStringArray(config.compilerOpts);
   cmdLen = config.compilerOpts.length;
 
diff --git a/lib/src/strings.dart b/lib/src/strings.dart
index 885427d..a4f1d68 100644
--- a/lib/src/strings.dart
+++ b/lib/src/strings.dart
@@ -34,6 +34,12 @@
 
 const compilerOpts = 'compiler-opts';
 
+const compilerOptsAuto = 'compiler-opts-automatic';
+// Sub-fields of compilerOptsAuto.
+const macos = 'macos';
+// Sub-fields of macos.
+const includeCStdLib = 'include-c-standard-library';
+
 // Declarations.
 const functions = 'functions';
 const structs = 'structs';
diff --git a/pubspec.yaml b/pubspec.yaml
index 31048c4..4104cd3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 name: ffigen
-version: 2.2.5
+version: 2.3.0
 homepage: https://github.com/dart-lang/ffigen
 description: Generator for FFI bindings, using LibClang to parse C header files.
 
diff --git a/test/config_tests/compiler_opts_test.dart b/test/config_tests/compiler_opts_test.dart
index a3aa117..1c5493b 100644
--- a/test/config_tests/compiler_opts_test.dart
+++ b/test/config_tests/compiler_opts_test.dart
@@ -2,8 +2,11 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:ffigen/ffigen.dart';
 import 'package:ffigen/src/code_generator.dart';
 import 'package:ffigen/src/config_provider/spec_utils.dart';
+import 'package:ffigen/src/strings.dart' as strings;
+import 'package:yaml/yaml.dart' as yaml;
 import 'package:test/test.dart';
 
 late Library actual, expected;
@@ -25,5 +28,19 @@
         ],
       );
     });
+    test('Compiler Opts Automatic', () {
+      final config = Config.fromYaml(yaml.loadYaml('''
+${strings.name}: 'NativeLibrary'
+${strings.description}: 'Compiler Opts Test'
+${strings.output}: 'unused'
+${strings.headers}:
+  ${strings.entryPoints}:
+    - 'test/header_parser_tests/comment_markup.h'
+${strings.compilerOptsAuto}:
+  ${strings.macos}:
+    ${strings.includeCStdLib}: false
+        ''') as yaml.YamlMap);
+      expect(config.compilerOpts.isEmpty, true);
+    });
   });
 }
diff --git a/test/header_parser_tests/dart_handle_test.dart b/test/header_parser_tests/dart_handle_test.dart
index 16387ff..27ce68e 100644
--- a/test/header_parser_tests/dart_handle_test.dart
+++ b/test/header_parser_tests/dart_handle_test.dart
@@ -25,7 +25,7 @@
 ${strings.name}: 'NativeLibrary'
 ${strings.description}: 'Dart_Handle Test'
 ${strings.output}: 'unused'
-${strings.compilerOpts}: '-I${path.join(getSdkPath(), "include")} -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Kernel.framework/Headers/'
+${strings.compilerOpts}: '-I${path.join(getSdkPath(), "include")}'
 
 ${strings.headers}:
   ${strings.entryPoints}:
diff --git a/test/header_parser_tests/globals_test.dart b/test/header_parser_tests/globals_test.dart
index db7bf8e..7da2414 100644
--- a/test/header_parser_tests/globals_test.dart
+++ b/test/header_parser_tests/globals_test.dart
@@ -36,8 +36,7 @@
       - myInt
       - pointerToLongDouble
       - globalStruct
-# Needed for stdbool.h in MacOS
-${strings.compilerOpts}: '-I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Kernel.framework/Headers/ -Wno-nullability-completeness'
+${strings.compilerOpts}: '-Wno-nullability-completeness'
         ''') as yaml.YamlMap),
       );
     });
diff --git a/test/large_integration_tests/large_test.dart b/test/large_integration_tests/large_test.dart
index 697ea92..2815a1f 100644
--- a/test/large_integration_tests/large_test.dart
+++ b/test/large_integration_tests/large_test.dart
@@ -79,8 +79,6 @@
 ${strings.description}: Bindings to SQLite.
 ${strings.output}: unused
 ${strings.arrayWorkaround}: true
-# Needed for stdarg.h in MacOS
-${strings.compilerOpts}: '-I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Kernel.framework/Headers/'
 ${strings.comments}:
   ${strings.style}: ${strings.any}
   ${strings.length}: ${strings.full}
diff --git a/test/native_test/config.yaml b/test/native_test/config.yaml
index 7abdce4..6c7b239 100644
--- a/test/native_test/config.yaml
+++ b/test/native_test/config.yaml
@@ -16,5 +16,4 @@
     - '**native_test.c'
 array-workaround: true
 
-# Needed for stdbool.h in MacOS
-compiler-opts: '-I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Kernel.framework/Headers/ -Wno-nullability-completeness'
+compiler-opts: '-Wno-nullability-completeness'
diff --git a/tool/libclang_config.yaml b/tool/libclang_config.yaml
index ec49446..f61c097 100644
--- a/tool/libclang_config.yaml
+++ b/tool/libclang_config.yaml
@@ -12,8 +12,9 @@
 name: Clang
 description: Holds bindings to LibClang.
 output: 'lib/src/header_parser/clang_bindings/clang_bindings.dart'
-sort: true
-compiler-opts: '-Ithird_party/libclang/include -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/ -Wno-nullability-completeness'
+compiler-opts:
+  - '-Ithird_party/libclang/include'
+  - '-Wno-nullability-completeness'
 headers:
   entry-points:
     - 'third_party/libclang/include/clang-c/Index.h'