Repository: ucb-bar/hammer (language: python)

Issue:
Refactor Hammer for PyPI Publishing
This is still a work in progress. The goals are:

- To be able to publish hammer to PyPI so tool and PDK plugins (and Chipyard) can depend on the published artifact
- Use poetry to set up a consistent development environment
- Get rid of all the PYTHONPATH hacks and the need to set HAMMER_VLSI
- Unify the testing scripts so a normal pytest invocation can be used
- Make CI scripts easy to write
- To the minimal extent possible, migrate to using pydantic models in lieu of jsonschema dynamic objects or NamedTuples

The things done so far are:

- Unify the hammer namespace by migrating all subprojects inside a top-level package called hammer
    - `hammer-vlsi/hammer_vlsi` -> `hammer.vlsi`
    - `hammer-vlsi/technology` -> `hammer.technology`
    - `hammer-vlsi/lvs` -> `hammer.lvs` (and so forth)
    - `hammer-tech` -> `hammer.tech`
    - `hammer-config` -> `hammer.config`
    - `hammer-shell` -> `hammer.shell`
- Dependencies are now specified in a pyproject.toml using the latest upstream versions, all submodules removed, no sourceme or other magic required to run tests
- Tests moved into a top-level folder (some tests have been migrated already, I'm working on the others)

To test this out:

1. Checkout the 'packaging' branch
1. Install poetry (https://python-poetry.org/docs/master/#installing-with-the-official-installer)
1. `poetry install` to fetch dependencies and set up a virtualenv
1. `poetry shell` to pop into the virtualenv, run `hammer-vlsi` as usual to run the CLI top-level
1. `pytest tests` to run the tests
1. `poetry build` to build a wheel


Discussion / hints:
Hey Vighnesh long time no see and nice to hear from you again! 1) Will this keep the `hammer-vlsi` CLI executable (which is quite useful)? 2) Will it be required to use `poetry` or pypi or will it still be possible to depend on hammer by source?
Hey Edward! Thanks looking over this.

1) Yes, `hammer-vlsi` is CLI executable when `hammer` is installed as a package. These 'installable' scripts are specified in `pyproject.toml`

```toml
[tool.poetry.scripts]
hammer-generate-mdf = "hammer.shell.hammer_generate_mdf:main"
get-config = "hammer.shell.get_config:main"
hammer-vlsi = "hammer.shell.hammer_vlsi:main"
```

2) You can always depend on hammer by source just by cloning it somewhere and adding `hammer/hammer` to your `PYTHONPATH`. Of course, the nicer way to do this is to install hammer as an 'editable local package' using `pip install -e` and that will allow you to use hammer in any virtualenv (or conda env) you want while being able to edit its source files and seeing the changes reflect immediately.
Big time support this, great work taking it on. 

A note on one of your comments: 

`Of course, the nicer way to do this is to install hammer as an 'editable local package' using pip install -e and that will allow you to use hammer in any virtualenv (or conda env) you want while being able to edit its source files and seeing the changes reflect immediately. `

The “editable installs” don’t quite place nice with Poetry / pyproject.toml, at least just yet. Or at least they didn’t as of a few months ago, when we decided to “revert” a bunch of projects (Hdl21, everything Vlsir-related) from pyproject.toml to setup.py. I can look up the problem if helpful, but I recall it was not work-around-able. 

I too would prefer the TOML config; the editable installs, which we use heavily, are the only thing holding it back. 

Good news is any of the features you want, including the ”scripts” / installed entry points, will work with either TOML or setup.py.  
I thought that newer versions of pip are able to handle delegating builds to poetry properly and still support locally editable packages that are only specified via pyproject.toml: https://github.com/python-poetry/poetry/issues/34#issuecomment-1054626460

https://github.com/python-poetry/poetry/issues/34#issuecomment-1193365526

> poetry-core also supports [PEP 660](https://peps.python.org/pep-0660/) in the stable and beta versions, which allows for a Poetry-managed project to be installed in editable mode by any PEP 517 frontend (e.g. Poetry itself, pip, etc) that also supports PEP 660.

But I haven't tried this out yet myself; I'll try it out with the tool plugin packages once I get this PR done
That would be great. I certainly had the impression they *intended* for it to work at the time. 

Looking back, it appears we abandoned Poetry in January: 
https://github.com/dan-fritchman/Hdl21/commit/afe44015c98e61cca84358b5a507efd8d5a2be22

And the commentary on the Poetry issue has all been since. Hopefully they’ve added this support.

@dpgrubb13 @harrisonliew This is ready for "review". I know that reviewing these changes line-by-line is going to be difficult, so it is better if you can set up the dev environment yourself and verify it works.

The documentation is here (https://github.com/ucb-bar/hammer/blob/packaging/doc/Hammer-Basics/Hammer-Setup.md#developer-setup). It should walk you through the process of setting up the dev environment, running tests, typechecking, testing multiple Python versions, and building the docs.

I've published this branch to TestPyPI (https://test.pypi.org/project/hammer-vlsi/) just to demonstrate the flow works.

While people are verifying that the dev flow works fine, I will use the published hammer-vlsi artifact to begin porting over the tool plugin repos. @dpgrubb13 will set up a strawman hammer-example repo which will use the published artifacts and implement a simple rtl2gds flow with asap7 and/or sky130 and the cadence toolchain.
I can confirm the dev flow works for me.
@harrisonliew 

1. Yes we should make sure the tests don't pollute the source directory. I need to hunt down where every emission comes from and make sure to redirect it by setting --obj_dir or running hammer inside a scratch test directory.
2. No, having py.typed in `hammer/hammer` is the right thing to do (https://peps.python.org/pep-0561/#packaging-type-information). I'm not sure why py.typed is not in the top-level ruamel.yaml package, but the hack works at least.
3. Yes that dependency is removed, and there is no need for json schema parsing and dynamic object construction anymore since TechJSON is just a Pydantic dataclass now (probably should rename TechJSON later when we do configuration as code). Yes, this means tech plugins need their json patched to convert spaces to underscores in keys. I've already done this for asap7 and it is pretty quick and easy so I anticipate it should be the same for other tech jsons.
4. The published gdstk includes the C++ library and the Python bindings together. The C++ .so needs to be built from source and the instructions in the asap7 README still apply (on BWRC machines we will have to compile gdstk manually with the documented hacks). I can clean up the asap7 readme with specific instructions.

Fix patch:
diff --git a/doc/Hammer-Basics/Hammer-Setup.md b/doc/Hammer-Basics/Hammer-Setup.md
index 3be160411..0062422f7 100644
--- a/doc/Hammer-Basics/Hammer-Setup.md
+++ b/doc/Hammer-Basics/Hammer-Setup.md
@@ -1,6 +1,6 @@
 # Hammer Setup
 
-Hammer depends on Python 3.8+.
+Hammer depends on Python 3.9+.
 
 The default technology, ASAP7, has some extra requirements. See its [README](https://github.com/ucb-bar/hammer/blob/master/hammer/technology/asap7/README.md) for instructions.
 
@@ -150,7 +150,7 @@ Success: no issues found in 25 source files
 
 ### Testing Different Python Versions with tox
 
-hammer is supposed to work with Python 3.8+, so we run its unit tests on all supported Python versions using `tox` and `pyenv`.
+hammer is supposed to work with Python 3.9+, so we run its unit tests on all supported Python versions using `tox` and `pyenv`.
 
 1. [Install `pyenv`](https://github.com/pyenv/pyenv-installer)
 
@@ -166,7 +166,6 @@ Then restart your shell again.
 See the `.python-version` file at the root of hammer and install those Python versions using `pyenv`.
 
 ```shell
-pyenv install 3.8.12
 pyenv install 3.9.13
 pyenv install 3.10.6
 ```
@@ -176,7 +175,6 @@ Once the Python interpreters are installed, run `pyenv versions` from the root o
 ```shell
 pyenv versions
   system
-* 3.8.12 (set by .../hammer/.python-version)
 * 3.9.13 (set by .../hammer/.python-version)
 * 3.10.6 (set by .../hammer/.python-version)
 ```
@@ -192,13 +190,13 @@ This will run the pytest unit tests using all the Python versions specified in `
 You can run tests only on a particular environment with `-e`
 
 ```shell
-tox -e py38 # only run tests on Python 3.8
+tox -e py39 # only run tests on Python 3.9
 ```
 
 You can pass command line arguments to the pytest invocation within a tox virtualenv with `--`
 
 ```shell
-tox -e py38 -- -k "lsf" -v
+tox -e py39 -- -k "lsf" -v
 ```
 
 ### Adding / Updating Dependencies
diff --git a/hammer/common/__init__.py b/hammer/common/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hammer/common/cadence/__init__.py b/hammer/common/cadence/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hammer/common/cadence/defaults_types.yml b/hammer/common/cadence/defaults_types.yml
new file mode 100644
index 000000000..6ac973391
--- /dev/null
+++ b/hammer/common/cadence/defaults_types.yml
@@ -0,0 +1,14 @@
+# Common Cadence settings.
+cadence:
+  # Directory where Cadence tools are installed. Contains subfolders like
+  # INNOVUS, GENUS, etc.
+  cadence_home: str
+
+  # License server information, to be overridden by some project generator.
+  # e.g. "port_a@server:port_b@server"
+  CDS_LIC_FILE: str
+
+  # Extra environment variables.
+  # List of dictionaries of variable-values.
+  # e.g. "MY_VAR=foo" -> [ {"MY_VAR": "foo"} ]
+  extra_env_vars: list[dict[str, str]]
diff --git a/hammer/common/mentor/__init__.py b/hammer/common/mentor/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hammer/common/mentor/defaults.yml b/hammer/common/mentor/defaults.yml
index f1d6ba2ac..b91f93f46 100644
--- a/hammer/common/mentor/defaults.yml
+++ b/hammer/common/mentor/defaults.yml
@@ -2,21 +2,17 @@
 mentor:
   # Directory where Mentor tools are installed. Contains subfolders like
   # calibre, modelsim, etc.
-  # type: str
   mentor_home: ""
 
   # License server information, to be overridden by some project generator.
   # e.g. "port_a@server:port_b@server"
-  # type: str
-  MGLS_LIC_FILE: ""
+  MGLS_LICENSE_FILE: ""
 
   # Extra environment variables.
   # List of dictionaries of variable-values.
   # e.g. "MY_VAR=foo" -> [ {"MY_VAR": "foo"} ]
-  # type: List[Dict[str, str]]
   extra_env_vars: []
 
   # Layer properties file for loading DRC/LVS results in Calibre.
   # If present, this is a path to said file.
-  # type: Optional[str]
   layerprops: null
diff --git a/hammer/common/mentor/defaults_types.yml b/hammer/common/mentor/defaults_types.yml
new file mode 100644
index 000000000..0a08bcc76
--- /dev/null
+++ b/hammer/common/mentor/defaults_types.yml
@@ -0,0 +1,18 @@
+# Common Mentor settings.
+mentor:
+  # Directory where Mentor tools are installed. Contains subfolders like
+  # calibre, modelsim, etc.
+  mentor_home: str
+
+  # License server information, to be overridden by some project generator.
+  # e.g. "port_a@server:port_b@server"
+  MGLS_LICENSE_FILE: str
+
+  # Extra environment variables.
+  # List of dictionaries of variable-values.
+  # e.g. "MY_VAR=foo" -> [ {"MY_VAR": "foo"} ]
+  extra_env_vars: list[dict[str, str]]
+
+  # Layer properties file for loading DRC/LVS results in Calibre.
+  # If present, this is a path to said file.
+  layerprops: Optional[str]
diff --git a/hammer/common/openroad/__init__.py b/hammer/common/openroad/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hammer/common/openroad/defaults_types.yml b/hammer/common/openroad/defaults_types.yml
new file mode 100644
index 000000000..21c044219
--- /dev/null
+++ b/hammer/common/openroad/defaults_types.yml
@@ -0,0 +1,6 @@
+# Common OpenROAD-flow settings.
+openroad:
+  # Extra environment variables.
+  # List of dictionaries of variable-values.
+  # e.g. "MY_VAR=foo" -> [ {"MY_VAR": "foo"} ]
+  extra_env_vars: list[dict[str, str]]
diff --git a/hammer/common/synopsys/__init__.py b/hammer/common/synopsys/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hammer/common/synopsys/defaults.yml b/hammer/common/synopsys/defaults.yml
new file mode 100644
index 000000000..a9851c80d
--- /dev/null
+++ b/hammer/common/synopsys/defaults.yml
@@ -0,0 +1,14 @@
+synopsys:
+  # Directory where Synopsys tools are installed.
+  synopsys_home: ""
+
+  # License server information, to be overridden by some project generator.
+  # e.g. "port_a@server:port_b@server"
+  SNPSLMD_LICENSE_FILE: ""
+  # TODO: this is actually a Mentor Graphics licence, not sure why the old dc scripts depend on it.
+  MGLS_LICENSE_FILE: ""
+
+  # Extra environment variables.
+  # List of dictionaries of variable-values.
+  # e.g. "MY_VAR=foo" -> [ {"MY_VAR": "foo"} ]
+  extra_env_vars: []
diff --git a/hammer/common/synopsys/defaults_types.yml b/hammer/common/synopsys/defaults_types.yml
new file mode 100644
index 000000000..53d70acf6
--- /dev/null
+++ b/hammer/common/synopsys/defaults_types.yml
@@ -0,0 +1,13 @@
+synopsys:
+  # Directory where Synopsys tools are installed.
+  synopsys_home: str
+
+  # License server information, to be overridden by some project generator.
+  # e.g. "port_a@server:port_b@server"
+  SNPSLMD_LICENSE_FILE: str
+  MGLS_LICENSE_FILE: str
+
+  # Extra environment variables.
+  # List of dictionaries of variable-values.
+  # e.g. "MY_VAR=foo" -> [ {"MY_VAR": "foo"} ]
+  extra_env_vars: list[dict[str, str]]
diff --git a/hammer/config/config_src.py b/hammer/config/config_src.py
index 0898bfd46..24c5c3005 100644
--- a/hammer/config/config_src.py
+++ b/hammer/config/config_src.py
@@ -12,7 +12,7 @@
 from enum import Enum
 from importlib import resources
 
-from hammer.utils import deepdict, topological_sort
+from hammer.utils import deepdict, add_dicts, topological_sort
 from .yaml2json import load_yaml  # grumble grumble
 
 from functools import reduce, lru_cache
@@ -30,6 +30,9 @@ def default(self, o):
             return float(o)
         return super(HammerJSONEncoder, self).default(o)
 
+# Special key used for meta directives which require config paths like prependlocal.
+_CONFIG_PATH_KEY = "_config_path"
+
 # Special key used to keep track of the next available integer suffix to avoid
 # duplicate keys.
 _NEXT_FREE_INDEX_KEY = "_next_free_index"
@@ -81,25 +84,38 @@ class MetaDirective(NamedTuple('MetaDirective', [
     __slots__ = ()
 
 
-def deepsubst_cwd(path: str) -> str:
+def deepsubst_cwd(config_dict: dict, path: str) -> str:
     """
     Prepend the current working directory (of the hammer runtime) to the beginning of the
     specified path.
 
+    :param config_dict: The original config dict (not used by this method).
     :param path: The string path to which the CWD is to be prepended.
-    :param params: The MetaDirectiveParams (not used by this method).
     :return: The path with CWD prepended.
     """
     # os.path.join handles the case where path is absolute
     # "If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component."
     return os.path.join(os.getcwd(), path)
 
-def deepsubst_transclude(path: str) -> str:
+def deepsubst_local(config_dict: dict, path: str) -> str:
+    """
+    Prepend the directory containing the config file containing this setting to the
+    beginning of the specified path.
+
+    :param config_dict: The original config dict.
+    :param path: The string path to which the local path is to be prepended.
+    :return: The path with local path of the config prepended.
+    """
+    # os.path.join handles the case where path is absolute
+    # "If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component."
+    return os.path.join(config_dict[_CONFIG_PATH_KEY], path)
+
+def deepsubst_transclude(config_dict: dict, path: str) -> str:
     """
     Load the path given as the new value of this key
 
+    :param config_dict: The original config dict (not used by this method).
     :param path: The string path to the file to be included
-    :param params: The MetaDirectiveParams which contain the local path.
     :return: The contents of the file at path
     """
     with open(path, "r", encoding="utf-8") as f:
@@ -108,8 +124,9 @@ def deepsubst_transclude(path: str) -> str:
 
 DeepSubstMetaDirectives = {
     "cwd": deepsubst_cwd,
+    "local": deepsubst_local,
     "transclude": deepsubst_transclude
-}  # type: Dict[str, Callable[[str], str]]
+}  # type: Dict[str, Callable[[Dict, str], str]]
 
 
 @lru_cache(maxsize=2)
@@ -356,6 +373,25 @@ def json2list_rename(key: str, value: Any, target_setting: str, replacement_sett
                                             target_settings=lambda key, value: [],
                                             rename_target=json2list_rename)
 
+    def prependlocal_action(config_dict: dict, key: str, value: Any) -> None:
+        """Prepend the local path of the config dict."""
+        if isinstance(value, list):
+            new_values = []
+            for v in value:
+                new_values.append(os.path.join(config_dict[_CONFIG_PATH_KEY], str(v)))
+            config_dict[key] = new_values
+        else:
+            config_dict[key] = os.path.join(config_dict[_CONFIG_PATH_KEY], str(value))
+
+    def prependlocal_rename(key: str, value: Any, target_setting: str, replacement_setting: str) -> Optional[
+        Tuple[Any, str]]:
+        # This metal directive doesn't depend on any settings
+        return value, "prependlocal"
+
+    directives['prependlocal'] = MetaDirective(action=prependlocal_action,
+                                               target_settings=lambda key, value: [],
+                                               rename_target=prependlocal_rename)
+
     def deepsubst_action(config_dict: dict, key: str, value: Any) -> None:
         """
         Perform a deep substitution on the value provided. This will replace any variables that occur in strings
@@ -384,7 +420,7 @@ def do_subst(oldval: Any) -> Any:
                                 meta = oldval[meta_key]
                                 if meta in DeepSubstMetaDirectives:
                                     if isinstance(v, str):
-                                        newval[k] = DeepSubstMetaDirectives[meta](v)
+                                        newval[k] = DeepSubstMetaDirectives[meta](config_dict, v)
                                     else:
                                         raise ValueError(f"Deepsubst metas not supported on non-string values: {v}")
                                 else:
@@ -526,6 +562,10 @@ def update_and_expand_meta(config_dict: dict, meta_dict: dict) -> dict:
     meta_dict_keys = list(meta_dict.keys())
     meta_keys = filter(lambda k: k.endswith("_meta"), meta_dict_keys)
 
+    # Update current config path to match meta dict's (used by prependlocal/deepsubst_local)
+    if _CONFIG_PATH_KEY in meta_dict_keys:
+        newdict[_CONFIG_PATH_KEY] = meta_dict[_CONFIG_PATH_KEY]
+
     # Deal with meta directives.
     meta_len = len("_meta")
     for meta_key in meta_keys:
@@ -651,7 +691,7 @@ def __init__(self) -> None:
 
         self.__config_types = {}  # type: dict
 
-        self.defaults = unpack(load_config_from_defaults("hammer.config")[0])
+        self.defaults = {}  # type: dict
 
     @property
     def runtime(self) -> List[dict]:
@@ -660,7 +700,7 @@ def runtime(self) -> List[dict]:
     @staticmethod
     def internal_keys() -> Set[str]:
         """Internal keys that shouldn't show up in any final config."""
-        return {_NEXT_FREE_INDEX_KEY}
+        return {_CONFIG_PATH_KEY, _NEXT_FREE_INDEX_KEY}
 
     def get_config(self) -> dict:
         """
@@ -706,21 +746,12 @@ def get_setting(self, key: str, nullvalue: Any = None, check_type: bool = True)
         :param check_type: Flag to enforce type checking
         :return: The given config
         """
-        #cfg_path = os.path.join(os.path.dirname(__file__), "..", "hammer-vlsi")
-        #defaults = unpack(load_config_from_defaults(cfg_path)[1])
-
-        IGNORE = ["vlsi.builtins.is_complete"]
         if key not in self.get_config():
             raise KeyError("Key " + key + " is missing")
-        if key not in IGNORE and key not in self.defaults:
+        if key not in self.defaults:
             warn(f"Key {key} does not have a default implementation")
-        if check_type and key not in IGNORE:
-            if self.get_config_types() == {}:
-                pass
-            elif key not in self.get_config_types():
-                warn(f"Key {key} is not associated with a type")
-            else:
-                self.check_setting(key)
+        if check_type:
+            self.check_setting(key)
         value = self.get_config()[key]
         return nullvalue if value is None else value
 
@@ -779,11 +810,21 @@ def check_setting(self, key: str, cfg: Optional[dict] = None) -> bool:
         """
         Checks a setting for correct typing.
         """
+        # Ignore all builtins
+        if any(key in unpack(builtin) for builtin in self.builtins):
+            return True
+
         if cfg is None:
             cfg = self.get_config()
-        exp_value_type = parse_setting_type(self.get_config_types()[key])
-        value = cfg[key]
+        if key not in self.get_config_types():
+            warn(f"Key {key} is not associated with a type")
+            return True
+        try:
+            exp_value_type = parse_setting_type(self.get_config_types()[key])
+        except ValueError as ve:
+            raise ValueError(f'Key {key} has an invalid outer type: perhaps you have "List" instead of "list" or "Dict" instead of "dict"?') from ve
 
+        value = cfg[key]
         if value is None and not exp_value_type.optional:
             raise TypeError(f"Key {key} is missing and non-optional")
         if value is None and exp_value_type.optional:
@@ -813,27 +854,31 @@ def check_setting(self, key: str, cfg: Optional[dict] = None) -> bool:
                     raise TypeError(f"Expected tertiary value type {exp_value_type.tertiary_v.value} for {key}, got type {v_type}")
         return True
 
-    def update_core(self, core_config: List[dict]) -> None:
+    def update_core(self, core_config: List[dict], core_config_types: List[dict]) -> None:
         """
         Update the core config with the given core config.
         """
-        for k in self.get_config_types().keys():
-            self.check_setting(k, cfg=combine_configs(core_config))
         self.core = core_config
+        self.update_defaults(core_config)
+        self.update_types(core_config_types, True)
         self.__config_cache_dirty = True
 
-    def update_tools(self, tools_config: List[dict]) -> None:
+    def update_tools(self, tools_config: List[dict], tool_config_types: List[dict]) -> None:
         """
         Update the tools config with the given tools config.
         """
         self.tools = tools_config
+        self.update_defaults(tools_config)
+        self.update_types(tool_config_types, True)
         self.__config_cache_dirty = True
 
-    def update_technology(self, technology_config: List[dict]) -> None:
+    def update_technology(self, technology_config: List[dict], technology_config_types: List[dict]) -> None:
         """
         Update the technology config with the given technology config.
         """
         self.technology = technology_config
+        self.update_defaults(technology_config)
+        self.update_types(technology_config_types, True)
         self.__config_cache_dirty = True
 
     def update_environment(self, environment_config: List[dict]) -> None:
@@ -857,12 +902,20 @@ def update_builtins(self, builtins_config: List[dict]) -> None:
         self.builtins = builtins_config
         self.__config_cache_dirty = True
 
+    def update_defaults(self, default_configs: List[dict]) -> None:
+        """
+        Update the default configs with the given config list.
+        This dict gets updated with each additional defaults config file.
+        """
+        for c in default_configs:
+            self.defaults = add_dicts(self.defaults, unpack(c))
+
     def update_types(self, config_types: List[dict], check_type: bool = True) -> None:
         """
         Update the types config with the given types config.
         """
         loaded_cfg = combine_configs(config_types)
-        self.__config_types = loaded_cfg
+        self.__config_types.update(loaded_cfg)
         if check_type:
             for k, v in loaded_cfg.items():
                 if not self.has_setting(k):
@@ -871,30 +924,44 @@ def update_types(self, config_types: List[dict], check_type: bool = True) -> Non
                     self.check_setting(k)
 
 
-def load_config_from_string(contents: str, is_yaml: bool) -> dict:
+def load_config_from_string(contents: str, is_yaml: bool, path: str = "unspecified") -> dict:
     """
     Load config from a string by loading it and unpacking it.
 
     :param contents: Contents of the config.
     :param is_yaml: True if the contents are yaml.
+    :param path: Path to the folder/package where the config file is located.
     :return: Loaded config dictionary, unpacked.
     """
     unpacked = unpack(load_yaml(contents) if is_yaml else json.loads(contents))
+    unpacked[_CONFIG_PATH_KEY] = path
     return unpacked
 
 
-def load_config_from_defaults(package: str, types: bool = False) -> List[dict]:
-    json_file = "defaults.json" if not types else "defaults_types.json"
-    yaml_file = "defaults.yml" if not types else "defaults_types.yml"
-    config_list: List[dict] = []
-    if importlib.resources.is_resource(package, json_file):
-        json_str = importlib.resources.read_text(package, json_file)
-        config_list.append(load_config_from_string(json_str, False))
-    if importlib.resources.is_resource(package, yaml_file):
-        yaml_str = importlib.resources.read_text(package, yaml_file)
-        config_list.append(load_config_from_string(yaml_str, True))
-    return config_list
+def load_config_from_defaults(package: str, types: bool = False) -> Tuple[List[dict], List[dict]]:
+    """
+    Load config from a package's defaults.
 
+    :param package: Package name
+    :param types: True if the types file(s) is to also be read
+    :return: Loaded config dictionary
+    """
+    package_path = importlib.resources.files(package)
+    json_file = package_path / "defaults.json"
+    json_types_file = package_path / "defaults_types.json"
+    yaml_file = package_path / "defaults.yml"
+    yaml_types_file = package_path / "defaults_types.yml"
+    config_list: List[dict] = []
+    config_types_list: List[dict] = []
+    if json_file.is_file():
+        config_list.append(load_config_from_string(json_file.read_text(), False, str(package_path)))
+    if json_types_file.is_file() and types:
+        config_types_list.append(load_config_from_string(json_types_file.read_text(), False, str(package_path)))
+    if yaml_file.is_file():
+        config_list.append(load_config_from_string(yaml_file.read_text(), True, str(package_path)))
+    if yaml_types_file.is_file() and types:
+        config_types_list.append(load_config_from_string(yaml_types_file.read_text(), True, str(package_path)))
+    return (config_list, config_types_list)
 
 def combine_configs(configs: Iterable[dict]) -> dict:
     """
diff --git a/hammer/config/defaults.yml b/hammer/config/defaults.yml
index 6395cf41e..2e189108b 100644
--- a/hammer/config/defaults.yml
+++ b/hammer/config/defaults.yml
@@ -472,8 +472,6 @@ par.inputs:
   # type: bool
   gds_merge: false
 
-  gds_map_resource: null
-
   physical_only_cells_mode: auto # Physical only cells mode.
   # type: str
   # The list of physical only cells are cells which don't have a logic function.
@@ -607,6 +605,8 @@ lvs.inputs:
 
   ilms: [] # Input list of ILMStructs
 
+  hcells_list: [] # Input list of LVS Hcells
+
   additional_lvs_text_mode: "auto" # Valid modes are auto, manual, prepend, and append
   additional_lvs_text: "" # Custom LVS command text to add after the boilerplate commands at the top of the run file
 
diff --git a/hammer/config/defaults_types.yml b/hammer/config/defaults_types.yml
index 3220ded20..42e67c84e 100644
--- a/hammer/config/defaults_types.yml
+++ b/hammer/config/defaults_types.yml
@@ -217,8 +217,6 @@ par.inputs:
   # Optional: GDS map file path. Used only if gds_map_mode above is set to manual.
   # type: Optional[str]
   gds_map_file: Optional[str]
-  # Optional: GDS map resource. Used if you want to point to a GDS map file inside a Python package.
-  gds_map_resource: Optional[list[str]]
 
   # If true, tell the place and route tool to merge any library/macro GDSes into
   # the final GDS.
@@ -337,6 +335,9 @@ lvs.inputs:
   # Input list of ILMStructs
   ilms: list[str]
 
+  # Input list of Hcells
+  hcells_list: list[str]
+
   # Valid modes are auto, manual, prepend, and append
   additional_lvs_text_mode: str
   # Custom LVS command text to add after the boilerplate commands at the top of the run file
diff --git a/hammer/pcb/generic/__init__.py b/hammer/pcb/generic/__init__.py
index ad025a45e..6eb87a3a3 100644
--- a/hammer/pcb/generic/__init__.py
+++ b/hammer/pcb/generic/__init__.py
@@ -292,7 +292,7 @@ def create_schematic_symbols(self) -> bool:
             else:
                 # Cap each group at 50
                 while group in groups and len(groups[group]) >= 50:
-                    match = re.match("^(.*)_(\d+)$", group)
+                    match = re.match(r"^(.*)_(\d+)$", group)
                     if match:
                         old = int(match.group(2))
                         group = match.group(1) + "_{new}".format(new=old+1)
diff --git a/hammer/synthesis/yosys/__init__.py b/hammer/synthesis/yosys/__init__.py
index 73143f85d..eb2a0fd23 100644
--- a/hammer/synthesis/yosys/__init__.py
+++ b/hammer/synthesis/yosys/__init__.py
@@ -55,7 +55,7 @@ def env_vars(self) -> Dict[str, str]:
     def post_synth_sdc(self) -> Optional[str]:
         # No post-synth SDC input for synthesis...
         return None
-    
+
     @property
     def all_regs_path(self) -> str:
         return os.path.join(self.run_dir, "find_regs_paths.json")
@@ -63,7 +63,7 @@ def all_regs_path(self) -> str:
     @property
     def all_cells_path(self) -> str:
         return os.path.join(self.run_dir, "find_regs_cells.json")
-    
+
     def fill_outputs(self) -> bool:
         # TODO: actually generate the following for simulation
         # Check that the regs paths were written properly if the write_regs step was run
@@ -121,7 +121,7 @@ def mapped_v_path(self) -> str:
     @property
     def mapped_sdc_path(self) -> str:
         return os.path.join(self.run_dir, "{}.mapped.sdc".format(self.top_module))
-    
+
     @property
     def mapped_blif_path(self) -> str:
         return os.path.join(self.run_dir, "{}.mapped.blif".format(self.top_module))
@@ -202,7 +202,7 @@ def init_environment(self) -> bool:
         self.synth_cap_load = 33.5 # SYNTH_CAP_LOAD
         self.max_fanout = 5 # default SYNTH_MAX_FANOUT = 5
 
-        
+
         self.driver_cell = None
         driver_cells = self.technology.get_special_cell_by_type(CellType.Driver)
         if len(driver_cells) == 0:
@@ -221,9 +221,9 @@ def init_environment(self) -> bool:
             corner_tt = next((corner for corner in corners if corner.type == MMMCCornerType.Extra), None)
         except:
             raise ValueError("An extra corner is required for Yosys.")
-            
+
         self.liberty_files_tt = self.get_timing_libs(corner_tt)
-        
+
         self.append("yosys -import")
 
         # replaces undef (x) constants with defined (0/1) constants.
@@ -231,13 +231,13 @@ def init_environment(self) -> bool:
 
         # We are switching working directories and Yosys still needs to find paths.
         abspath_input_files = list(map(lambda name: os.path.join(os.getcwd(), name), self.input_files))  # type: List[str]
-        
+
         # Add any verilog_synth wrappers (which are needed in some technologies e.g. for SRAMs) which need to be
         # synthesized.
         abspath_input_files += self.technology.read_libs([
             hammer_tech.filters.verilog_synth_filter
         ], hammer_tech.HammerTechnologyUtils.to_plain_item)
-        
+
         for verilog_file in abspath_input_files:
             self.append(f"read_verilog -sv {verilog_file}")
 
@@ -251,17 +251,9 @@ def syn_generic(self) -> bool:
         # TODO: is there a better way to do this? like self.get_setting()
         if self._database.has_setting("synthesis.yosys.latch_map_file"):
             latch_map = f"techmap -map {self.get_setting('synthesis.yosys.latch_map_file')}"
-        elif self._database.has_setting("synthesis.yosys.latch_map_resource"):
-            path = self.get_setting("synthesis.yosys.latch_map_resource")
-            package = path[0]
-            resource = path[1]
-            text = importlib.resources.files(package).joinpath(resource).read_text()
-            latch_map_file = (Path(self.technology.cache_dir) / resource)
-            latch_map_file.write_text(text)
-            latch_map = f"techmap -map {str(latch_map_file)}"
         else:  # TODO: make the else case better
             latch_map = ""
-        
+
         self.block_append(f"""
         # TODO: verify this command, it was in yosys manual but not in OpenLANE script
         yosys proc
@@ -289,7 +281,7 @@ def syn_generic(self) -> bool:
         return True
 
     def syn_map(self) -> bool:
-        
+
         self.block_append(f"""
         # Technology mapping for cells
         # ABC supports multiple liberty files, but the hook from Yosys to ABC doesn't
@@ -319,7 +311,7 @@ def add_tieoffs(self) -> bool:
 
         if len(tie_hi_cells) != 1 or len (tie_lo_cells) != 1 or tie_hi_cells[0].output_ports is None or tie_lo_cells[0].output_ports is None:
             self.logger.warning("Hi and Lo tiecells and their input ports are unspecified or improperly specified and will not be added during synthesis.")
-        else:   
+        else:
             tie_hi_cell = tie_hi_cells[0].name[0]
             tie_hi_port = tie_hi_cells[0].output_ports[0]
             tie_lo_cell = tie_lo_cells[0].name[0]
@@ -351,7 +343,7 @@ def generate_reports(self) -> bool:
         tee -o {self.run_dir}/{self.top_module}.synth_stat.txt stat -top {self.top_module} -liberty {self.liberty_files_tt.split()[0]}
         """)
         return True
-    
+
     def write_outputs(self) -> bool:
         hier_mapped_v_path=os.path.join(self.run_dir, f"{self.top_module}.mapped.hier.v")
         self.block_append(f"""
@@ -367,5 +359,5 @@ def write_outputs(self) -> bool:
         """)
         self.ran_write_outputs = True
         return True
-    
+
 tool = YosysSynth
diff --git a/hammer/synthesis/yosys/defaults.yml b/hammer/synthesis/yosys/defaults.yml
index 540ae5080..0bbc1baf4 100644
--- a/hammer/synthesis/yosys/defaults.yml
+++ b/hammer/synthesis/yosys/defaults.yml
@@ -13,5 +13,3 @@ synthesis.yosys:
   # type: Optional[str]
   # Used by Yosys to do technology mapping of latches
   # Should be set in technology plugin
-  
-  
\ No newline at end of file
diff --git a/hammer/synthesis/yosys/defaults_types.yml b/hammer/synthesis/yosys/defaults_types.yml
index 15c408fe1..2ac440d41 100644
--- a/hammer/synthesis/yosys/defaults_types.yml
+++ b/hammer/synthesis/yosys/defaults_types.yml
@@ -16,8 +16,3 @@ synthesis.yosys:
   # Used by Yosys to do technology mapping of latches
   # Should be set in technology plugin
   latch_map_file: Optional[str]
-
-  # Optional: Latch map file (as a Python resource)
-  # Set as ["package name", "resource name"]
-  # e.g. ["hammer.technology.asap7.extra", "latch_map.v"]
-  latch_map_resource: Optional[list[str]]
\ No newline at end of file
diff --git a/hammer/tech/__init__.py b/hammer/tech/__init__.py
index f29e6d668..d79e7ee78 100644
--- a/hammer/tech/__init__.py
+++ b/hammer/tech/__init__.py
@@ -364,16 +364,14 @@ def load_from_module(cls, tech_module: str) -> Optional["HammerTechnology"]:
         tech.name = technology_name
         tech.package = tech_module
 
-        tech_json = f"{technology_name}.tech.json"
-        tech_yaml = f"{technology_name}.tech.yml"
+        tech_json = importlib.resources.files(tech_module) / f"{technology_name}.tech.json"
+        tech_yaml = importlib.resources.files(tech_module) / f"{technology_name}.tech.yml"
 
-        if importlib.resources.is_resource(tech_module, tech_json):
-            json_str = importlib.resources.read_text(tech_module, tech_json)
-            tech.config = TechJSON.parse_raw(json_str)
+        if tech_json.is_file():
+            tech.config = TechJSON.parse_raw(tech_json.read_text())
             return tech
-        elif importlib.resources.is_resource(tech_module, tech_yaml):
-            json_str = json.dumps(load_yaml(importlib.resources.read_text(tech_module, tech_yaml)))
-            tech.config = TechJSON.parse_raw(json_str)
+        elif tech_yaml.is_file():
+            tech.config = TechJSON.parse_raw(json.dumps(load_yaml(tech_yaml.read_text())))
             return tech
         else:
             return None
@@ -402,10 +400,10 @@ def has_setting(self, key: str) -> bool:
         """
         return self._database.has_setting(key)
 
-    def get_config(self) -> List[dict]:
+    def get_config(self) -> Tuple[List[dict], List[dict]]:
         """Get the hammer configuration for this technology. Not to be confused with the ".tech.json" which
         self.config refers to. """
-        return hammer_config.load_config_from_defaults(self.package)
+        return hammer_config.load_config_from_defaults(self.package, types=True)
 
     @property
     def dont_use_list(self) -> Optional[List[str]]:
@@ -636,16 +634,13 @@ def prepend_dir_path(self, path: str, lib: Optional[Library] = None) -> str:
         if os.path.isabs(path):
             return path
 
-        # 2. If the path has no path separator, the path is relative to the tech plugin package itself
-        #    Copy the resource into the cache dir and return a pointer to it
+        # 2. If the path has no path separator, the path is relative to the tech plugin package itself.
+        #    Do not need to copy the resource into the cache dir because poetry packages all resources into site-packages.
         if os.sep not in path:
-            assert importlib.resources.is_resource(self.package, path),\
+            resource_path = importlib.resources.files(self.package) / path
+            assert resource_path.is_file(),\
                 f"{path} wasn't found in HammerTechnology Python package {self.package}"
-            resource_txt = importlib.resources.read_text(self.package, path)
-            resource_path = os.path.join(self.cache_dir, path)
-            with open(resource_path, "w") as f:
-                f.write(resource_txt)
-            return resource_path
+            return str(resource_path)
 
         # 3-5. The path consists of an identifier (prefix) and the rest of the path now
         id = path.split(os.path.sep)[0]
@@ -906,17 +901,21 @@ def filter_for_supplies(self, lib: Library) -> bool:
         :param lib: Library to check
         :return: True if the supplies of this library match the inputs for this run, False otherwise.
         """
-        if lib.supplies is None:
-            # TODO: add some sort of wildcard value for supplies for libraries which _actually_ should
-            # always be used.
-            self.logger.warning("Lib %s has no supplies annotation! Using anyway." % (lib.json()))
-            return True
         # If we are using MMMC assume all libraries will be used.
         # TODO: Read the corners and filter out libraries that don't match any of them.
         # Requires a refactor because MMMCCorner parsing is only in HammerTool now.
         # See issue #275.
         if self.get_setting("vlsi.inputs.mmmc_corners"):
             return True
+        if lib.supplies is None:
+            # TODO: add some sort of wildcard value for supplies for libraries which _actually_ should
+            # always be used.
+            if lib.provides is not None:
+                for provided in lib.provides:
+                    if provided.lib_type is not None and provided.lib_type == "technology":
+                        return True
+            self.logger.warning("Lib %s has no supplies annotation! Using anyway." % (lib.json()))
+            return True
         return self.get_setting("vlsi.inputs.supplies.VDD") == lib.supplies.VDD and self.get_setting(
             "vlsi.inputs.supplies.GND") == lib.supplies.GND
 
diff --git a/hammer/tech/stackup.py b/hammer/tech/stackup.py
index 34b552038..4fd41d249 100644
--- a/hammer/tech/stackup.py
+++ b/hammer/tech/stackup.py
@@ -102,7 +102,7 @@ class Metal(BaseModel):
     pitch: Decimal
     offset: Decimal
     power_strap_widths_and_spacings: List[WidthSpacingTuple]
-    power_strap_width_table: Optional[List[Decimal]] = []
+    power_strap_width_table: List[Decimal] = []
     # Note: grid_unit is not currently parsed as part of the Metal data structure!
     # See #379
     grid_unit: Decimal
diff --git a/hammer/technology/asap7/defaults.yml b/hammer/technology/asap7/defaults.yml
index d6ca05e8c..16b95011d 100644
--- a/hammer/technology/asap7/defaults.yml
+++ b/hammer/technology/asap7/defaults.yml
@@ -47,8 +47,8 @@ technology.core:
   tap_cell_rail_reference: "{TAPCELL*}" # This is used to provide a reference master for generating standard cells
 
 synthesis.yosys:
-  latch_map_resource: ["hammer.technology.asap7", "latch_map.v"]
-  latch_map_resource_meta: lazysubst
+  latch_map_file: "latch_map.v"
+  latch_map_file_meta: prependlocal
 
 par.inputs:
   gds_merge: true # gds_merge: true is mandatory,
@@ -81,7 +81,7 @@ par.generate_power_straps_options:
 
 power.voltus:
   lef_layer_map: "lefdefLayer.map"
-  #lef_layer_map_meta: "prependlocal"
+  lef_layer_map_meta: prependlocal
 
 mentor.extra_env_vars:
   - VAR_M4_YOFFSET: "0"
@@ -89,6 +89,6 @@ mentor.extra_env_vars:
   - VAR_M6_YOFFSET: "0"
   - VAR_M7_XOFFSET: "0"
 mentor.layerprops: "calibre.layerprops"
-#mentor.layerprops_meta: "prependlocal"
+mentor.layerprops_meta: prependlocal
 drc.calibre.version: "2017.3_38.30" # 2017-year only Calibre supported by decks
 lvs.calibre.version: "2017.3_38.30"
diff --git a/hammer/technology/asap7/defaults_types.yml b/hammer/technology/asap7/defaults_types.yml
new file mode 100644
index 000000000..0c83e2b9b
--- /dev/null
+++ b/hammer/technology/asap7/defaults_types.yml
@@ -0,0 +1,5 @@
+# Settings for the asap7 technology to be overriden by the project.
+technology.asap7:
+  tarball_dir: str
+  pdk_install_dir: str
+  stdcell_install_dir: str
diff --git a/hammer/technology/asap7/sram_compiler/__init__.py b/hammer/technology/asap7/sram_compiler/__init__.py
index 4fed83762..f15a24f99 100644
--- a/hammer/technology/asap7/sram_compiler/__init__.py
+++ b/hammer/technology/asap7/sram_compiler/__init__.py
@@ -197,28 +197,22 @@ def generate_sram(self, params: SRAMParameters, corner: MMMCCorner) -> ExtraLibr
 """.format(NUMADDR=math.ceil(math.log2(params.depth)), NUMWORDS=params.depth, WORDLENGTH=params.width, NAME=sram_name,
            RAND_WIDTH=math.ceil(params.width / 32), specify=specify))
 
-        # lib_path ="{t}/{n}_{c}.lib".format(t=tech_cache_dir,n=sram_name,c=corner_str)
-        # if not os.path.exists(lib_path):
-        #    os.symlink("{b}/lib/{n}_lib/{n}_{c}.lib".format(b=base_dir,n=sram_name,c=corner_str),
-        #    "{t}/{n}_{c}.lib".format(t=tech_cache_dir,n=sram_name,c=corner_str))
+        package_dir = importlib.resources.files(self.package)
+
         nldm_lib_file = f"{sram_name}_{corner_str}.lib"
         lef_file = f"{sram_name}_x4.lef"
         gds_file = f"{sram_name}_x4.gds"
 
-        nldm_lib_str = importlib.resources.read_text(f"{self.package}.memories.lib.{sram_name}_lib", nldm_lib_file)
-        lef_str = importlib.resources.read_text(f"{self.package}.memories.lef", lef_file)
-        gds_str = importlib.resources.read_binary(f"{self.package}.memories.gds", gds_file)
-
-        (Path(tech_cache_dir) / nldm_lib_file).write_text(nldm_lib_str)
-        (Path(tech_cache_dir) / lef_file).write_text(lef_str)
-        (Path(tech_cache_dir) / gds_file).write_bytes(gds_str)
+        nldm_lib_dir = package_dir / f"memories/lib/{sram_name}_lib"
+        lef_dir = package_dir / "memories/lef"
+        gds_dir = package_dir / "memories/gds"
 
         from hammer.tech import Corner, Supplies, Provide
         lib = ExtraLibrary(prefix=None, library=Library(
             name=sram_name,
-            nldm_liberty_file=f"{tech_cache_dir}/{nldm_lib_file}",
-            lef_file=f"{tech_cache_dir}/{lef_file}",
-            gds_file=f"{tech_cache_dir}/{gds_file}",
+            nldm_liberty_file=f"{nldm_lib_dir}/{nldm_lib_file}",
+            lef_file=f"{lef_dir}/{lef_file}",
+            gds_file=f"{gds_dir}/{gds_file}",
             # verilog_sim="{b}/behavioral/sram_behav_models.v".format(b=base_dir),
             verilog_sim=verilog_path,
             corner=Corner(nmos=speed_name, pmos=speed_name, temperature=str(corner.temp.value_in_units("C")) + " C"),
diff --git a/hammer/technology/sky130/defaults.yml b/hammer/technology/sky130/defaults.yml
index a82f6fa6b..7025f4ce5 100644
--- a/hammer/technology/sky130/defaults.yml
+++ b/hammer/technology/sky130/defaults.yml
@@ -93,12 +93,12 @@ synthesis.yosys:
 par.inputs:
   gds_merge: true
   gds_map_mode: manual
-  gds_map_resource: ["hammer.technology.sky130.extra", "sky130_lefpin.map"]
-  gds_map_resource_meta: lazysubst
+  gds_map_file: "extra/sky130_lefpin.map"
+  gds_map_file_meta: prependlocal
 
 par.openroad:
-  openrcx_techfile: ["hammer.technology.sky130.extra", "sky130hd.rcx_rules"]
-  openrcx_techfile_meta: lazysubst
+  openrcx_techfile: "extra/sky130hd.rcx_rules"
+  openrcx_techfile_meta: prependlocal
   klayout_techfile_source: "${technology.sky130.sky130A}/libs.tech/klayout/sky130A.lyt"
   klayout_techfile_source_meta: lazysubst
 
diff --git a/hammer/utils/lef_utils.py b/hammer/utils/lef_utils.py
index 41e5b6519..da526ad98 100644
--- a/hammer/utils/lef_utils.py
+++ b/hammer/utils/lef_utils.py
@@ -29,7 +29,7 @@ def get_sizes(source: str) -> List[Tuple[str, Decimal, Decimal]]:
         found_size = False  # type: bool
         for line in lines:
             # Check for PROPERTYDEFINITIONS statement
-            regex = "^\s*PROPERTYDEFINITIONS"
+            regex = r"^\s*PROPERTYDEFINITIONS"
             regex_search = re.search(regex, line)
 
             if regex_search:
@@ -50,7 +50,7 @@ def get_sizes(source: str) -> List[Tuple[str, Decimal, Decimal]]:
                 continue
 
             # Check for MACRO statement
-            regex = "MACRO\s+([a-zA-Z0-9_]+)"
+            regex = r"MACRO\s+([a-zA-Z0-9_]+)"
             regex_search = re.search(regex, line)
 
             if regex_search:
@@ -74,7 +74,7 @@ def get_sizes(source: str) -> List[Tuple[str, Decimal, Decimal]]:
                 continue
 
             # Check for SIZE
-            regex = "SIZE\s+([\d\.]+)\s+[bB][yY]\s+([\d\.]+)\s*;"
+            regex = r"SIZE\s+([\d\.]+)\s+[bB][yY]\s+([\d\.]+)\s*;"
             regex_search = re.search(regex, line)
 
             if regex_search:
diff --git a/hammer/utils/verilog_utils.py b/hammer/utils/verilog_utils.py
index b8752693f..c0b3f351a 100644
--- a/hammer/utils/verilog_utils.py
+++ b/hammer/utils/verilog_utils.py
@@ -37,7 +37,7 @@ def contains_module(v: str, module: str) -> bool:
         # Remove comments first to avoid false matches in the comments
         new_v = VerilogUtils.remove_comments(v)
 
-        regex = "module\s+" + re.escape(module)
+        regex = r"module\s+" + re.escape(module)
         return re.search(regex, new_v) is not None
 
     @staticmethod
@@ -53,5 +53,5 @@ def remove_module(v: str, module: str) -> str:
             # Don't risk touching the source if we don't think the module exists.
             return v
 
-        regex = "(module\s+" + re.escape(module) + ".+?endmodule)"
+        regex = r"(module\s+" + re.escape(module) + ".+?endmodule)"
         return re.sub(regex, "", v, flags=re.DOTALL)
diff --git a/hammer/vlsi/cli_driver.py b/hammer/vlsi/cli_driver.py
index 43eb8dfbc..255d9ddfb 100644
--- a/hammer/vlsi/cli_driver.py
+++ b/hammer/vlsi/cli_driver.py
@@ -1336,7 +1336,7 @@ def args_to_driver(self, args: dict,
         project_configs_yaml: List[dict] = []
         for conf_file in project_configs:
             config_str = Path(conf_file).read_text()
-            project_configs_yaml.append(hammer.config.load_config_from_string(config_str, is_yaml=True))
+            project_configs_yaml.append(hammer.config.load_config_from_string(config_str, is_yaml=True, path=str(Path(conf_file).resolve().parent)))
         project_configs_yaml_keys = [set(i.keys()) for i in project_configs_yaml]
         key_history: Dict[str, List[str]] = {i: [] for i in reduce(lambda x, y: x.union(y), project_configs_yaml_keys)}
         for cfg_file, cfg in zip(project_configs, project_configs_yaml_keys):
diff --git a/hammer/vlsi/driver.py b/hammer/vlsi/driver.py
index d5e566a97..4297af388 100644
--- a/hammer/vlsi/driver.py
+++ b/hammer/vlsi/driver.py
@@ -86,7 +86,7 @@ def __init__(self, options: HammerDriverOptions, extra_project_config: dict = {}
                 self.log.error("Environment config %s does not exist!" % (config))
             config_str = Path(config).read_text()
             is_yaml = config.endswith(".yml")
-            self.database.update_environment([hammer_config.load_config_from_string(config_str, is_yaml)])
+            self.database.update_environment([hammer_config.load_config_from_string(config_str, is_yaml, str(Path(config).resolve().parent))])
 
         # Read in the project config to find the syn, par, and tech.
         project_configs: List[dict] = []
@@ -95,7 +95,7 @@ def __init__(self, options: HammerDriverOptions, extra_project_config: dict = {}
                 self.log.error("Project config %s does not exist!" % (config))
             config_str = Path(config).read_text()
             is_yaml = config.endswith(".yml")
-            project_configs.append(hammer_config.load_config_from_string(config_str, is_yaml))
+            project_configs.append(hammer_config.load_config_from_string(config_str, is_yaml, str(Path(config).resolve().parent)))
         project_configs.append(extra_project_config)
         self.project_configs = []  # type: List[dict]
         self.update_project_configs(project_configs)
@@ -107,6 +107,7 @@ def __init__(self, options: HammerDriverOptions, extra_project_config: dict = {}
         # Keep track of what the synthesis and par configs are since
         # update_tools() just takes a whole list.
         self.tool_configs = {}  # type: Dict[str, List[dict]]
+        self.tool_config_types = {}  # type: Dict[str, List[dict]]
 
         # Initialize tool fields.
         self.syn_tool = None  # type: Optional[HammerSynthesisTool]
@@ -157,7 +158,7 @@ def load_technology(self, cache_dir: str = "") -> None:
         else:
             tech: hammer_tech.HammerTechnology = tech_opt
         # Update database as soon as possible since e.g. extract_technology_files could use those settings
-        self.database.update_technology(tech.get_config())
+        self.database.update_technology(*tech.get_config())
         tech.logger = self.log.context("tech")
         tech.set_database(self.database)
         tech.cache_dir = cache_dir
@@ -170,7 +171,8 @@ def update_tool_configs(self) -> None:
         Calls self.database.update_tools with self.tool_configs as a list.
         """
         tools = reduce(lambda a, b: a + b, list(self.tool_configs.values()))
-        self.database.update_tools(tools)
+        types = reduce(lambda a, b: a + b, list(self.tool_config_types.values()))
+        self.database.update_tools(tools, types)
 
     def instantiate_tool_from_config(self, tool_type: str,
                                      required_type: Optional[type] = None) -> Optional[Tuple[HammerTool, str]]:
@@ -210,7 +212,6 @@ def set_up_synthesis_tool(self, syn_tool: HammerSynthesisTool,
 
         if run_dir == "":
             run_dir = os.path.join(self.obj_dir, "syn-rundir")
-        print(syn_tool)
         # TODO: generate this automatically
         syn_tool.name = name
         syn_tool.logger = self.log.context("synthesis")
@@ -237,7 +238,7 @@ def set_up_synthesis_tool(self, syn_tool: HammerSynthesisTool,
             return False
 
         self.syn_tool = syn_tool
-        self.tool_configs["synthesis"] = syn_tool.get_config()
+        self.tool_configs["synthesis"], self.tool_config_types["synthesis"] = syn_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -305,7 +306,7 @@ def set_up_par_tool(self, par_tool: HammerPlaceAndRouteTool,
             return False
 
         self.par_tool = par_tool
-        self.tool_configs["par"] = par_tool.get_config()
+        self.tool_configs["par"], self.tool_config_types["par"] = par_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -367,7 +368,7 @@ def set_up_drc_tool(self, drc_tool: HammerDRCTool,
             return False
 
         self.drc_tool = drc_tool
-        self.tool_configs["drc"] = drc_tool.get_config()
+        self.tool_configs["drc"], self.tool_config_types["drc"] = drc_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -434,7 +435,7 @@ def set_up_lvs_tool(self, lvs_tool: HammerLVSTool,
             return False
 
         self.lvs_tool = lvs_tool
-        self.tool_configs["lvs"] = lvs_tool.get_config()
+        self.tool_configs["lvs"], self.tool_config_types["lvs"] = lvs_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -488,7 +489,7 @@ def set_up_sram_generator_tool(self, sram_generator_tool: HammerSRAMGeneratorToo
             self.log.warning("No SRAM parameters specified, no SRAMs will be generated.")
 
         self.sram_generator_tool = sram_generator_tool
-        self.tool_configs["sram_generator"] = sram_generator_tool.get_config()
+        self.tool_configs["sram_generator"], self.tool_config_types["sram_generator"] = sram_generator_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -560,7 +561,7 @@ def set_up_sim_tool(self, sim_tool: HammerSimTool,
             return False
 
         self.sim_tool = sim_tool
-        self.tool_configs["simulation"] = sim_tool.get_config()
+        self.tool_configs["simulation"], self.tool_config_types["simulation"] = sim_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -628,7 +629,7 @@ def set_up_power_tool(self, power_tool: HammerPowerTool,
             return False
 
         self.power_tool = power_tool
-        self.tool_configs["power"] = power_tool.get_config()
+        self.tool_configs["power"], self.tool_config_types["power"] = power_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -698,7 +699,7 @@ def set_up_formal_tool(self, formal_tool: HammerFormalTool,
 
         self.formal_tool = formal_tool
 
-        self.tool_configs["formal"] = formal_tool.get_config()
+        self.tool_configs["formal"], self.tool_config_types["formal"] = formal_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -758,7 +759,7 @@ def set_up_timing_tool(self, timing_tool: HammerTimingTool,
 
         self.timing_tool = timing_tool
 
-        self.tool_configs["timing"] = timing_tool.get_config()
+        self.tool_configs["timing"], self.tool_config_types["timing"] = timing_tool.get_config()
         self.update_tool_configs()
         return True
 
@@ -809,7 +810,7 @@ def set_up_pcb_tool(self, pcb_tool: HammerPCBDeliverableTool,
         pcb_tool.run_dir = run_dir
 
         self.pcb_tool = pcb_tool
-        self.tool_configs["pcb"] = pcb_tool.get_config()
+        self.tool_configs["pcb"], self.tool_config_types["pcb"] = pcb_tool.get_config()
         self.update_tool_configs()
         return True
 
diff --git a/hammer/vlsi/hammer_tool.py b/hammer/vlsi/hammer_tool.py
index 3e0256e99..bb793e304 100644
--- a/hammer/vlsi/hammer_tool.py
+++ b/hammer/vlsi/hammer_tool.py
@@ -11,7 +11,6 @@
 from functools import reduce
 from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, cast
 from inspect import cleandoc
-import importlib.resources
 from pathlib import Path
 
 import hammer.config as hammer_config
@@ -165,9 +164,9 @@ def run_persistent_step(self, pst: HammerToolHookAction, target_step: HammerTool
         if pst.location == HookLocation.PersistentStep:
             self.logger.debug("Running persistent sub-step '{pstep}' before '{step}'".format(pstep=pst.step.name, step=target_step.name))
         if pst.location == HookLocation.PersistentPreStep:
-            self.logger.debug("Running persistent sub-step '{pstep}' before '{step}' (pre-step: '{pre_step}'".format(pstep=pst.step.name, step=target_step.name, pre_step=pst.target_name))
+            self.logger.debug("Running persistent sub-step '{pstep}' before '{step}' (pre-step: '{pre_step}')".format(pstep=pst.step.name, step=target_step.name, pre_step=pst.target_name))
         if pst.location == HookLocation.PersistentPostStep:
-            self.logger.debug("Running persistent sub-step '{pstep}' after '{step}' (post-step: '{post_step}'".format(pstep=pst.step.name, step=target_step.name, post_step=pst.target_name))
+            self.logger.debug("Running persistent sub-step '{pstep}' after '{step}' (post-step: '{post_step}')".format(pstep=pst.step.name, step=target_step.name, post_step=pst.target_name))
         pst_out = pst.step.func(self)
         assert pst_out, "Persistent step {step} failed!".format(step=pst.step.name)
         return pst_out
@@ -884,9 +883,9 @@ def dump_database(self) -> str:
             f.write(db_contents)
         return path
 
-    def get_config(self) -> List[dict]:
+    def get_config(self) -> Tuple[List[dict], List[dict]]:
         """Get the config for this tool."""
-        return hammer_config.load_config_from_defaults(self.package)
+        return hammer_config.load_config_from_defaults(self.package, types=True)
 
     def get_setting(self, key: str, nullvalue: Any = None) -> Any:
         """
@@ -1383,9 +1382,6 @@ def get_gds_map_file(self) -> Optional[str]:
         manual_map_file: Optional[str] = str(self.get_setting("par.inputs.gds_map_file")) if self.get_setting(
             "par.inputs.gds_map_file") is not None else None
 
-        manual_map_resource: Optional[List[str]] = self.get_setting("par.inputs.gds_map_resource") if self.get_setting(
-            "par.inputs.gds_map_resource") is not None else None
-
         # tech_map_file will only be used in auto mode
         tech_map_file_raw = self.technology.config.gds_map_file
         tech_map_file_optional = str(
@@ -1398,11 +1394,7 @@ def get_gds_map_file(self) -> Optional[str]:
             if manual_map_file:
                 map_file = manual_map_file
             else:
-                assert manual_map_resource
-                text = importlib.resources.files(manual_map_resource[0]).joinpath(manual_map_resource[1]).read_text()
-                manual_map_resource_path = Path(self.technology.cache_dir) / manual_map_resource[1]
-                manual_map_resource_path.write_text(text)
-                map_file = str(manual_map_resource_path)
+                raise ValueError("par.inputs.gds_map_mode set to manual but no par.inputs.gds_map_file specified!")
         elif gds_map_mode == "empty":
             map_file = None
         else:
diff --git a/hammer/vlsi/hammer_vlsi_impl.py b/hammer/vlsi/hammer_vlsi_impl.py
index 8fb576163..3ba28e12c 100644
--- a/hammer/vlsi/hammer_vlsi_impl.py
+++ b/hammer/vlsi/hammer_vlsi_impl.py
@@ -110,16 +110,21 @@ def load_builtins_and_core(cls, database: hammer_config.HammerDatabase) -> None:
         """
 
         # Load in builtins.
-        builtins_yml = resources.read_text("hammer.config", "builtins.yml")
+        builtins_yml = resources.files("hammer.config") / "builtins.yml"
         database.update_builtins([
-            hammer_config.load_config_from_string(builtins_yml, True),#, ("hammer.config", "builtins.yml")),
+            hammer_config.load_config_from_string(builtins_yml.read_text(), True),
             HammerVLSISettings.get_config()
         ])
 
         # Read in core defaults.
-        database.update_core(hammer_config.load_config_from_defaults("hammer.config"))
-        database.update_types(hammer_config.load_config_from_defaults("hammer.config", types=True))
-
+        database.update_core(*hammer_config.load_config_from_defaults("hammer.config", types=True))
+
+        # Read in vendor-common defaults.
+        # TODO: move these defaults into respective vendor plugin packages
+        for pkg in ["hammer.common.cadence", "hammer.common.synopsys", "hammer.common.mentor", "hammer.common.openroad"]:
+            config, types = hammer_config.load_config_from_defaults(pkg, types=True)
+            database.update_defaults(config)
+            database.update_types(types, check_type=False)
 
 from .hammer_tool import HammerTool, HammerToolStep
 
diff --git a/poetry.lock b/poetry.lock
index 698b6595e..f96577840 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -124,6 +124,17 @@ python-versions = ">=3.7"
 docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
 testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
 
+[[package]]
+name = "gdspy"
+version = "1.4"
+description = "Python module for creating/importing/merging GDSII files."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+numpy = "*"
+
 [[package]]
 name = "gdstk"
 version = "0.9.34"
@@ -749,12 +760,12 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker
 testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
 
 [extras]
-asap7 = ["gdstk"]
+asap7 = ["gdspy"]
 
 [metadata]
 lock-version = "1.1"
-python-versions = "^3.8"
-content-hash = "0ca1c187b346bf4a6f65c816366103472e9764a162ab81244d173ef500042c92"
+python-versions = "^3.9"
+content-hash = "0d6a06f27022f3e7dc713fb5bcd42f861cab0853a95af9cc1f700ccdc3aed879"
 
 [metadata.files]
 alabaster = [
@@ -805,6 +816,9 @@ filelock = [
     {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
     {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
 ]
+gdspy = [
+    {file = "gdspy-1.4.zip", hash = "sha256:0ce8a159d253399ba5092692548b5b6990cbf3d1febd9117f79400cb7a5426df"},
+]
 gdstk = [
     {file = "gdstk-0.9.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a609397d749db507b998e5308fc8abf61786c2363ac58a74dd3f72c73be0e672"},
     {file = "gdstk-0.9.34-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:348f6444409229e82ad78f8058725ea59eb03736af41292a9d0c02a4be28d6bd"},
diff --git a/pyproject.toml b/pyproject.toml
index 0adb82642..96bbe4e05 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,7 +20,7 @@ yaml2json = "hammer.shell.yaml2json:main"
 asap7_gds_scale = "hammer.technology.asap7.gds_scale:main"
 
 [tool.poetry.dependencies]
-python = "^3.8"
+python = "^3.9"
 pydantic = "^1.9.2"
 PyYAML = "^6.0"
 "ruamel.yaml" = "^0.17.21"


Test patch:
diff --git a/e2e/configs/asap7-test.yml b/e2e/configs/asap7-test.yml
index 7ef07fbff..6da9a5abd 100644
--- a/e2e/configs/asap7-test.yml
+++ b/e2e/configs/asap7-test.yml
@@ -52,7 +52,4 @@ vlsi.core.drc_tool: "hammer.drc.calibre"
 vlsi.core.lvs_tool: "hammer.lvs.calibre"
 
 # SRAM Compiler compiler options
-vlsi.core.sram_generator_tool: "sram_compiler"
-# You should specify a location for the SRAM generator in the tech plugin
-vlsi.core.sram_generator_tool_path: ["hammer/src/hammer-vlsi/technology/asap7"]
-vlsi.core.sram_generator_tool_path_meta: "append"
+vlsi.core.sram_generator_tool: "hammer.technology.asap7.sram_compiler"
diff --git a/e2e/hammer-cadence-plugins b/e2e/hammer-cadence-plugins
index c80a17e0f..1042c355f 160000
--- a/e2e/hammer-cadence-plugins
+++ b/e2e/hammer-cadence-plugins
@@ -1,1 +1,1 @@
-Subproject commit c80a17e0fd7b4c713988e51463b85c06c5185903
+Subproject commit 1042c355fa00edda8726a6c4609f614f8e969c86
diff --git a/tests/test_build_systems.py b/tests/test_build_systems.py
index 2e232ea0f..b8046b10b 100644
--- a/tests/test_build_systems.py
+++ b/tests/test_build_systems.py
@@ -66,7 +66,7 @@ def test_flat_makefile(self, tmpdir) -> None:
         bridge_tasks = {"syn-to-sim", "syn-to-par", "par-to-sim", "par-to-lvs", "par-to-drc", "syn-to-power", "par-to-power", "sim-rtl-to-power", "sim-syn-to-power", "sim-par-to-power", "syn-to-formal", "par-to-formal", "syn-to-timing", "par-to-timing"}
         expected_targets = tasks.copy()
         expected_targets.update(bridge_tasks)
-        expected_targets.update({"redo-" + x for x in expected_targets if x is not "pcb"})
+        expected_targets.update({"redo-" + x for x in expected_targets if x != "pcb"})
         expected_targets.update({os.path.join(tmpdir, x + "-rundir", x + "-output-full.json") for x in tasks if x not in {"sim-rtl", "sim-syn", "sim-par", "power-rtl", "power-syn", "power-par", "formal-syn", "formal-par", "timing-syn", "timing-par"}})
         expected_targets.update({os.path.join(tmpdir, x + "-rundir", x.split('-')[0] + "-output-full.json") for x in tasks if x in {"sim-rtl", "sim-syn", "sim-par", "power-rtl", "power-syn", "power-par", "formal-syn", "formal-par", "timing-syn", "timing-par"}})
         expected_targets.update({os.path.join(tmpdir, x + "-input.json") for x in tasks if x not in {"syn", "pcb", "sim-rtl", "power-rtl"}})
diff --git a/tests/test_config.py b/tests/test_config.py
index bcbfab55a..93396aa1f 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -7,11 +7,15 @@
 import warnings
 import pytest
 import importlib.resources
+from typing import List, Dict
 
 import hammer.config as hammer_config
 
 
 class TestHammerDatabase:
+    # For tests that don't check against any types
+    NO_TYPES = []  # type: List[Dict]
+
     def test_overriding(self) -> None:
         """
         Test that we can add a project first and technology after and still have it override.
@@ -19,7 +23,7 @@ def test_overriding(self) -> None:
         db = hammer_config.HammerDatabase()
         db.update_project([{"tech.x": "foo"}])
         assert db.get_setting("tech.x", check_type=False) == "foo"
-        db.update_technology([{"tech.x": "bar"}])
+        db.update_technology([{"tech.x": "bar"}], self.NO_TYPES)
         assert db.get_setting("tech.x", check_type=False) == "foo"
 
     def test_unpacking(self) -> None:
@@ -33,15 +37,15 @@ def test_unpacking(self) -> None:
         adc: "yes"
         dac: "no"
 """, is_yaml=True)
-        db.update_core([config])
+        db.update_core([config], self.NO_TYPES)
         assert db.get_setting("foo.bar.adc", check_type=False) == "yes"
         assert db.get_setting("foo.bar.dac", check_type=False) == "no"
 
     def test_no_config_junk(self) -> None:
         """Test that no _config_path junk variables get left behind."""
         db = hammer_config.HammerDatabase()
-        db.update_core([hammer_config.load_config_from_string("key1: value1", is_yaml=True)])
-        db.update_technology([hammer_config.load_config_from_string("key2: value2", is_yaml=True)])
+        db.update_core([hammer_config.load_config_from_string("key1: value1", is_yaml=True)], self.NO_TYPES)
+        db.update_technology([hammer_config.load_config_from_string("key2: value2", is_yaml=True)], self.NO_TYPES)
         db.update_project([hammer_config.load_config_from_string("key3: value3", is_yaml=True)])
         for key in hammer_config.HammerDatabase.internal_keys():
             assert db.has_setting(key) is False, "Should not have internal key " + key
@@ -59,11 +63,11 @@ def test_repeated_updates(self) -> None:
 a.b.c: ["test"]
 a.b.c_meta: append
 """, is_yaml=True)
-        db.update_core([base])
+        db.update_core([base], self.NO_TYPES)
         assert db.get_setting("a.b.c", check_type=False) == []
         db.update_project([meta])
         assert db.get_setting("a.b.c", check_type=False) == ["test"]
-        db.update_technology([])
+        db.update_technology([], self.NO_TYPES)
         assert db.get_setting("a.b.c", check_type=False) == ["test"]
         db.update_environment([])
         assert db.get_setting("a.b.c", check_type=False) == ["test"]
@@ -82,11 +86,11 @@ def test_no_json_yaml_precedence(self) -> None:
         db1 = hammer_config.HammerDatabase()
         yaml_config = hammer_config.load_config_from_string(yaml, is_yaml=True)
         json_config = hammer_config.load_config_from_string(json, is_yaml=False)
-        db1.update_core([hammer_config.combine_configs([yaml_config, json_config])])
+        db1.update_core([hammer_config.combine_configs([yaml_config, json_config])], self.NO_TYPES)
         assert db1.get_setting("foo.bar", check_type=False) == "i'm json"
 
         db2 = hammer_config.HammerDatabase()
-        db2.update_core([hammer_config.combine_configs([json_config, yaml_config])])
+        db2.update_core([hammer_config.combine_configs([json_config, yaml_config])], self.NO_TYPES)
         assert db2.get_setting("foo.bar", check_type=False) == "i'm yaml"
 
     def test_meta_json2list(self) -> None:
@@ -105,7 +109,7 @@ def test_meta_json2list(self) -> None:
     "foo.pipeline_meta": "json2list"
 }
     """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.flash", check_type=False) == "yes"
         assert db.get_setting("foo.max", check_type=False) == "min"
         assert db.get_setting("foo.pipeline", check_type=False) == ["1", "2"]
@@ -127,7 +131,7 @@ def test_meta_lazyjson2list(self) -> None:
     "foo.pipeline": "[\\"1\\", \\"2\\"]"
 }
     """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.flash", check_type=False) == "yes"
         assert db.get_setting("foo.max", check_type=False) == "min"
         assert db.get_setting("foo.pipeline", check_type=False) == ["1", "2"]
@@ -151,7 +155,7 @@ def test_meta_subst(self) -> None:
   "foo.uint_meta": "subst"
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.flash", check_type=False) == "yes"
         assert db.get_setting("foo.pipeline", check_type=False) == "yesman"
         assert db.get_setting("foo.uint", check_type=False) == ["1", "2"]
@@ -187,7 +191,7 @@ def test_meta_lazysubst(self) -> None:
   "style": "agile"
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         db.update_project([project])
         assert db.get_setting("foo.flash", check_type=False) == "yes"
         assert db.get_setting("foo.pipeline", check_type=False) == "yesman"
@@ -205,7 +209,7 @@ def test_meta_lazysubst_array(self) -> None:
         array: ["${target}bar"]
         array_meta: lazysubst
         """, is_yaml=True)
-        db.update_core([d])
+        db.update_core([d], self.NO_TYPES)
         assert db.get_setting("array", check_type=False) == ["foobar"]
 
     def test_meta_append(self) -> None:
@@ -227,7 +231,7 @@ def test_meta_append(self) -> None:
   "foo.bar.dac": "current_weighted"
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.bar.dac", check_type=False) == "current_weighted"
         assert db.get_setting("foo.bar.dsl", check_type=False) == ["scala", "python"]
 
@@ -245,7 +249,7 @@ def test_meta_crossappend(self) -> None:
   "foo.bar.languages_meta": "crossappend"
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.bar.dsl", check_type=False) == ["scala"]
         assert db.get_setting("foo.bar.languages", check_type=False) == ["scala", "python"]
 
@@ -264,7 +268,7 @@ def test_meta_crossappendref(self) -> None:
   "foo.bar.languages_meta": "crossappendref"
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.bar.dsl", check_type=False) == ["scala"]
         assert db.get_setting("foo.bar.languages", check_type=False) == ["scala", "python"]
 
@@ -293,7 +297,7 @@ def test_meta_crossref(self) -> None:
   "indirect.numbers_meta": ["subst", "crossref"]
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("just.numbers", check_type=False) == ["1", "2", "3"]
         assert db.get_setting("copies.numbers", check_type=False) == [["1", "2", "3"], ["world"]]
         assert db.get_setting("bools", check_type=False) == [False, True]
@@ -314,7 +318,7 @@ def test_meta_lazycrossref(self) -> None:
 lazy.numbers: "numbers"
 lazy.numbers_meta: lazycrossref
     """, is_yaml=True)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("lazy.numbers", check_type=False) == ["1", "2", "3"]
 
     def test_meta_crossref_errors(self) -> None:
@@ -337,7 +341,7 @@ def test_meta_crossref_errors(self) -> None:
   "no_crossrefing_using_int_meta": "crossref"
 }
 """, is_yaml=False)
-            db.update_core([base, meta])
+            db.update_core([base, meta], self.NO_TYPES)
             db.get_setting("no_crossrefing_using_int", check_type=False)
         with pytest.raises(ValueError):
             meta = hammer_config.load_config_from_string("""
@@ -346,7 +350,7 @@ def test_meta_crossref_errors(self) -> None:
   "no_crossrefing_using_bool_meta": "crossref"
 }
 """, is_yaml=False)
-            db.update_core([base, meta])
+            db.update_core([base, meta], self.NO_TYPES)
             db.get_setting("no_crossrefing_using_bool", check_type=False)
         with pytest.raises(ValueError):
             meta = hammer_config.load_config_from_string("""
@@ -355,9 +359,45 @@ def test_meta_crossref_errors(self) -> None:
   "bad_list_meta": "crossref"
 }
 """, is_yaml=False)
-            db.update_core([base, meta])
+            db.update_core([base, meta], self.NO_TYPES)
             db.get_setting("bad_list", check_type=False)
 
+    def test_meta_prependlocal(self) -> None:
+        """
+        Test that the meta attribute "prependlocal" works.
+        """
+        db = hammer_config.HammerDatabase()
+        base = hammer_config.load_config_from_string("""
+foo:
+    bar:
+        adc: "yes"
+        dac: "no"
+        dsl: ["scala"]
+        base_test: "local_path"
+        base_test_meta: prependlocal
+""", is_yaml=True, path="base/config/path")
+        meta = hammer_config.load_config_from_string("""
+{
+  "foo.bar.dsl": ["python"],
+  "foo.bar.dsl_meta": "append",
+  "foo.bar.dac": "current_weighted",
+  "foo.bar.meta_test": "local_path",
+  "foo.bar.meta_test_meta": "prependlocal"
+}
+""", is_yaml=False, path="meta/config/path")
+        array = hammer_config.load_config_from_string("""
+foo:
+    bar:
+        array_test: ["local_path", "express_path"]
+        array_test_meta: prependlocal
+""", is_yaml=True, path="array/config/path")
+        db.update_core([base, meta, array], self.NO_TYPES)
+        assert db.get_setting("foo.bar.dac", check_type=False) == "current_weighted"
+        assert db.get_setting("foo.bar.dsl", check_type=False) == ["scala", "python"]
+        assert db.get_setting("foo.bar.base_test", check_type=False) == "base/config/path/local_path"
+        assert db.get_setting("foo.bar.meta_test", check_type=False) == "meta/config/path/local_path"
+        assert db.get_setting("foo.bar.array_test", check_type=False) == ["array/config/path/local_path", "array/config/path/express_path"]
+
     def test_meta_transclude(self) -> None:
         """
         Test that the meta attribute "transclude" works.
@@ -381,7 +421,7 @@ def test_meta_transclude(self) -> None:
 }
 """.replace("<path>", path), is_yaml=False)
 
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
 
         # Trigger merge before cleanup
         assert db.get_setting("chips.potato", check_type=False) == "tuber"
@@ -417,7 +457,7 @@ def test_meta_transclude_subst(self) -> None:
 }
 """.replace("<path>", path), is_yaml=False)
 
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
 
         # Trigger merge before cleanup
         assert db.get_setting("food.dish", check_type=False) == "chips"
@@ -443,7 +483,7 @@ def test_meta_as_array_1(self) -> None:
   "foo.bar.meta_test_meta": ["subst"]
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.bar.base_test", check_type=False) == "local_path"
         assert db.get_setting("foo.bar.meta_test", check_type=False) == "local_path"
 
@@ -465,7 +505,7 @@ def test_multiple_lazy_metas(self) -> None:
     "base": "hello2"
 }
     """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         with pytest.raises(ValueError) as cm:
             assert db.get_setting("base", check_type=False) == "hello2"
             assert db.get_setting("test", check_type=False) == "def"
@@ -538,7 +578,7 @@ def test_meta_lazy_referencing_other_lazy(self) -> None:
   "global": "foobar"
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("global", check_type=False) == "foobar"
         assert db.get_setting("tool1.common", check_type=False) == "foobar tool1"
         assert db.get_setting("tool1.a", check_type=False) == "foobar tool1 abc"
@@ -569,7 +609,7 @@ def test_meta_lazysubst_other_lazysubst(self) -> None:
   "later_meta": "lazysubst"
 }
     """, is_yaml=False)
-        db.update_core([base])
+        db.update_core([base], self.NO_TYPES)
         db.update_project([project])
         assert db.get_setting("lolcat", check_type=False) == "whatever"
         assert db.get_setting("foo.twelve", check_type=False) == "whatever"
@@ -588,7 +628,7 @@ def test_meta_lazycrossappendref(self) -> None:
 tool: ["tool_1", "snakes"]
 tool_meta: "lazycrossappendref"
 """, is_yaml=True)
-        db.update_core([base])
+        db.update_core([base], self.NO_TYPES)
         assert db.get_setting("global", check_type=False) == ["hello", "world", "scala"]
         assert db.get_setting("tool", check_type=False) == ["hello", "world", "scala", "python"]
 
@@ -604,7 +644,7 @@ def test_meta_lazycrossappend_with_lazycrossref(self) -> None:
 tool: ["tool_1", ["python"]]
 tool_meta: "lazycrossappend"
 """, is_yaml=True)
-        db.update_core([base])
+        db.update_core([base], self.NO_TYPES)
         assert db.get_setting("global", check_type=False) == ["hello", "world", "scala"]
         assert db.get_setting("tool", check_type=False) == ["hello", "world", "scala", "python"]
 
@@ -626,7 +666,7 @@ def test_meta_lazyappend_with_lazycrossref_2(self) -> None:
   "tool_meta": "lazyappend"
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("global", check_type=False) == ["hello", "world", "scala"]
         assert db.get_setting("tool", check_type=False) == ["hello", "world", "scala", "python"]
 
@@ -652,7 +692,7 @@ def test_self_reference_lazysubst(self) -> None:
     "derivative_str_meta": "lazysubst"
 }
         """, is_yaml=True)
-        db.update_core([base, config1, config2])
+        db.update_core([base, config1, config2], self.NO_TYPES)
         assert db.get_setting("derivative_str", check_type=False) == "hello_1_2"
 
     def test_self_reference_lazycrossref(self) -> None:
@@ -678,7 +718,7 @@ def test_self_reference_lazycrossref(self) -> None:
     "derivative_meta": "lazycrossref"
 }
         """, is_yaml=True)
-        db.update_core([base, config1, config2])
+        db.update_core([base, config1, config2], self.NO_TYPES)
         assert db.get_setting("base", check_type=False) == "tower"
 
     def test_self_reference_lazyappend(self) -> None:
@@ -702,7 +742,7 @@ def test_self_reference_lazyappend(self) -> None:
   "global_meta": "lazyappend"
 }
 """, is_yaml=False)
-        db.update_core([base, config1])
+        db.update_core([base, config1], self.NO_TYPES)
         db.update_project([config2])
         assert db.get_setting("global", check_type=False) == ["hello", "world", "scala", "python"]
 
@@ -736,12 +776,47 @@ def test_meta_deepsubst_cwd(self) -> None:
   }]
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.bar", check_type=False)[0]["base_test"] == os.path.join(cwd, "some_relative_path")
         assert db.get_setting("foo.bar", check_type=False)[1]["meta_test"] == os.path.join(cwd, "some/relative/path")
         # leading / should override the meta
         assert db.get_setting("foo.bar", check_type=False)[1]["abs_test"] == "/this/is/an/abs/path"
 
+    def test_meta_deepsubst_local(self) -> None:
+        """
+        Test that the deepsubst special meta "local" correctly prepends the local path.
+        """
+        db = hammer_config.HammerDatabase()
+        base = hammer_config.load_config_from_string("""
+foo:
+    bar_meta: deepsubst
+    bar:
+    - adc: "yes"
+      dac: "no"
+      dsl: ["scala"]
+      base_test: "local_path"
+      base_test_deepsubst_meta: "local"
+""", is_yaml=True, path="base/config/path")
+        meta = hammer_config.load_config_from_string("""
+{
+  "foo.bar_meta": ["append", "deepsubst"],
+  "foo.bar": [{
+      "dsl": ["python"],
+      "dsl_meta": "append",
+      "dac": "current_weighted",
+      "meta_test": "local_path",
+      "meta_test_deepsubst_meta": "local",
+      "abs_test": "/this/is/an/abs/path",
+      "abs_test_deepsubst_meta": "local"
+  }]
+}
+""", is_yaml=False, path="meta/config/path")
+        db.update_core([base, meta], self.NO_TYPES)
+        assert db.get_setting("foo.bar", check_type=False)[0]["base_test"] == "base/config/path/local_path"
+        assert db.get_setting("foo.bar", check_type=False)[1]["meta_test"] == "meta/config/path/local_path"
+        # leading / should override the meta
+        assert db.get_setting("foo.bar", check_type=False)[1]["abs_test"] == "/this/is/an/abs/path"
+
     def test_meta_deepsubst_subst(self) -> None:
         """
         Test that deepsubst correctly substitutes strings.
@@ -768,7 +843,7 @@ def test_meta_deepsubst_subst(self) -> None:
   }]
 }
 """, is_yaml=False)
-        db.update_core([base, meta])
+        db.update_core([base, meta], self.NO_TYPES)
         assert db.get_setting("foo.bar", check_type=False)[0]["dsl"] == {"x": ["python", "is", "awesome"], "y": "sometimes"}
 
     def test_load_types(self) -> None:
@@ -825,9 +900,9 @@ def test_wrong_types(self) -> None:
         wrong: list[dict[str, str]]
 """, is_yaml=True)
 
-        db.update_core([base])
+        db.update_core([base], self.NO_TYPES)
         with pytest.raises(TypeError):
-            db.update_types([base_types])
+            db.update_core([base], [base_types])
 
         assert db.get_setting("foo.bar.adc") == [1, 2, 3]
 
@@ -853,9 +928,9 @@ def test_wrong_constraints(self) -> None:
         wrong: int
 """, is_yaml=True)
 
-        db.update_core([base])
+        db.update_core([base], self.NO_TYPES)
         with pytest.raises(TypeError):
-            db.update_types([base_types])
+            db.update_core([base], [base_types])
 
         assert db.get_setting("foo.bar.adc") == [{"name": "hi", "pin": 1}]
         assert db.get_setting("foo.bar.dac") == [{"name": ["hi"], "pin": [1, 2]}]
@@ -879,8 +954,7 @@ def test_optional_constraints(self) -> None:
         quu: Optional[str]
 """, is_yaml=True)
 
-        db.update_core([base])
-        db.update_types([base_types])
+        db.update_core([base], [base_types])
 
         assert db.get_setting("foo.bar.adc") is None
         assert db.get_setting("foo.bar.quu") == "hi"
@@ -903,9 +977,9 @@ def test_decimal_constraints(self) -> None:
         quu: float
 """, is_yaml=True)
 
-        db.update_core([base])
+        db.update_core([base], self.NO_TYPES)
         with pytest.raises(TypeError):
-            db.update_types([base_types])
+            db.update_core([base], [base_types])
 
         assert db.get_setting("foo.bar.adc") == 3.14
 
@@ -914,15 +988,13 @@ def test_default_types(self) -> None:
         Test that all HAMMER defaults are properly type-checked.
         """
 
-        base = hammer_config.load_config_from_defaults("hammer.config")
-        base_types = hammer_config.load_config_from_defaults("hammer.config", types=True)
-        builtins_yml = importlib.resources.read_text("hammer.config", "builtins.yml")
+        base, base_types = hammer_config.load_config_from_defaults("hammer.config", types=True)
+        builtins_yml = importlib.resources.files("hammer.config").joinpath("builtins.yml").read_text()
         builtins = hammer_config.load_config_from_string(builtins_yml, is_yaml=True)
 
         db = hammer_config.HammerDatabase()
-        db.update_core(base)
+        db.update_core(base, base_types)
         db.update_builtins([builtins])
-        db.update_types(base_types)
 
         for k, v in db.get_config().items():
             assert db.get_setting(k) == v
diff --git a/tests/test_signoff_tools.py b/tests/test_signoff_tools.py
index 54e6247c0..0e05be114 100644
--- a/tests/test_signoff_tools.py
+++ b/tests/test_signoff_tools.py
@@ -39,7 +39,7 @@ def signoff_test_context(tmp_path, tool_type: str) -> Iterator[SignoffToolTestCo
         "%s.temp_folder" % tool_type: temp_dir,
         "%s.submit.command" % tool_type: "local"
     }  # type: Dict[str, Any]
-    if tool_type is "lvs":
+    if tool_type == "lvs":
         json_content.update({
             "lvs.inputs.schematic_files": ["/dev/null"],
             "lvs.inputs.hcells_list": []
diff --git a/tests/test_submit_command.py b/tests/test_submit_command.py
index bfab05cbc..9c96977ce 100644
--- a/tests/test_submit_command.py
+++ b/tests/test_submit_command.py
@@ -44,10 +44,9 @@ def submit_command_test_context(tmp_path, cmd_type: str) -> Iterator[SubmitComma
         "synthesis.mocksynth.temp_folder": temp_dir,
         "synthesis.submit.command": cmd_type
     }
-    if cmd_type is "lsf":
-        assert resources.is_resource("utils", "mock_bsub.sh")
-        with resources.path("utils", "mock_bsub.sh") as b:
-            bsub_binary = str(b)
+    if cmd_type == "lsf":
+        assert resources.files("utils").joinpath("mock_bsub.sh").is_file()
+        bsub_binary = str(resources.files("utils").joinpath("mock_bsub.sh"))
         json_content.update({
             "synthesis.submit.settings": [{"lsf": {
                 "queue": "myqueue",
diff --git a/tests/test_tech.py b/tests/test_tech.py
index 36689e528..9389668b8 100644
--- a/tests/test_tech.py
+++ b/tests/test_tech.py
@@ -186,7 +186,7 @@ def test_tarballs_not_extracted(self, tmp_path, request) -> None:
         tech.cache_dir = tech_dir
 
         database = hammer_config.HammerDatabase()
-        database.update_technology(tech.get_config())
+        database.update_technology(*tech.get_config())
         HammerVLSISettings.load_builtins_and_core(database)
         tech.set_database(database)
         outputs = tech.process_library_filter(pre_filts=[], filt=hammer_tech.filters.gds_filter,
@@ -219,7 +219,7 @@ def test_tarballs_pre_extracted(self, tmpdir, request) -> None:
         tech.cache_dir = tech_dir
 
         database = hammer_config.HammerDatabase()
-        database.update_technology(tech.get_config())
+        database.update_technology(*tech.get_config())
         HammerVLSISettings.load_builtins_and_core(database)
         tech.set_database(database)
         outputs = tech.process_library_filter(pre_filts=[], filt=hammer_tech.filters.gds_filter,
@@ -254,7 +254,7 @@ def test_tarballs_pre_extracted_tech_specific(self, tmpdir, request) -> None:
         tech.cache_dir = tech_dir
 
         database = hammer_config.HammerDatabase()
-        database.update_technology(tech.get_config())
+        database.update_technology(*tech.get_config())
         HammerVLSISettings.load_builtins_and_core(database)
         tech.set_database(database)
         outputs = tech.process_library_filter(pre_filts=[], filt=hammer_tech.filters.gds_filter,
@@ -351,7 +351,7 @@ def test_installs_in_cache_dir(self, tmpdir, request) -> None:
         tech.cache_dir = tech_dir
 
         database = hammer_config.HammerDatabase()
-        database.update_technology(tech.get_config())
+        database.update_technology(*tech.get_config())
         HammerVLSISettings.load_builtins_and_core(database)
         tech.set_database(database)
         outputs = tech.process_library_filter(pre_filts=[], filt=hammer_tech.filters.lef_filter,