Repository: qir-alliance/pyqir (language: python)

Issue:
Cargo git dependency causes slow clone of llvm repo
If one uses qirlib as a cargo dependency using the git specification, 
e.g. in `Cargo.toml`

```
qirlib = {path ="https://github.com/qir-alliance/pyqir"}
```

Cargo attempts to clone llvm-project as a submodule, which is a very large repository, and does not appear to be required for the dependency.

I don't have a good suggested fix, nor do I know if this actually requires fixing, just something I thought was worth being aware of.

Currently my workaround is to clone the repository myself without submodules and have a local path dependency.


Discussion / hints:

Fix patch:
diff --git a/.cargo/config.toml.template b/.cargo/config.toml.template
deleted file mode 100644
index 4d6cd125..00000000
--- a/.cargo/config.toml.template
+++ /dev/null
@@ -1,18 +0,0 @@
-[target.x86_64-apple-darwin]
-rustflags = [
-  "-C", "link-arg=-undefined",
-  "-C", "link-arg=dynamic_lookup",
-]
-
-[target.aarch64-apple-darwin]
-rustflags = [
-  "-C", "link-arg=-undefined",
-  "-C", "link-arg=dynamic_lookup",
-]
-
-[target.x86_64-pc-windows-msvc]
-rustflags = [
-  "-C", "target-feature=+crt-static",
-  "-C", "control-flow-guard",
-]
-
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index da86c1e5..1abd22b1 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -4,13 +4,6 @@ FROM mcr.microsoft.com/vscode/devcontainers/universal:1-linux
 
 USER root
 
-# Install CMake 3.20 (required since apt-get uses 3.16 and repo requires 3.20)
-RUN curl -SsL https://github.com/Kitware/CMake/releases/download/v3.20.5/cmake-3.20.5-linux-x86_64.sh -o cmakeinstall.sh \
-    && echo "f582e02696ceee81818dc3378531804b2213ed41c2a8bc566253d16d894cefab cmakeinstall.sh" | sha256sum -c --strict - \
-    && chmod +x cmakeinstall.sh \
-    && ./cmakeinstall.sh --prefix=/usr/local --exclude-subdir \
-    && rm cmakeinstall.sh
-
 RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
     && apt-get install -y --no-install-recommends ninja-build clang-11 clang-tidy-11 build-essential \
     && apt-get clean \
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f0bd1b78..744c5498 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,9 @@ on:
 
 env:
   CARGO_TERM_COLOR: always
+  QIRLIB_DOWNLOAD_LLVM: false
+  CCACHE_DIR: ~/.ccache
+  SCCACHE_DIR: C:\sccache
 
 jobs:
   build:
@@ -39,18 +42,40 @@ jobs:
       - uses: actions/checkout@v2
         with:
           submodules: 'recursive'
-      - name: Linux - Install build dependencies, ccache, and update PATH to use linked versions of gcc, cc, etc
+      - name: Linux - Install build dependencies, ccache, ninja
         run: sudo apt-get install -y ccache ninja-build
         if: ${{ matrix.config.os == 'ubuntu-20.04' }}
-      - name: Windows - Install build dependencies, ccache, and update PATH to use linked versions of gcc, cc, etc
+      - name: Windows - Install build dependencies, sccache, ninja
         run: choco install --accept-license -y sccache ninja
         if: ${{ matrix.config.os == 'windows-2019' }}
-      - name: MacOS - Install build dependencies, ccache, and update PATH to use linked versions of gcc, cc, etc
+      - name: MacOS - Install build dependencies, ccache, ninja
         run: brew install ccache ninja
         if: ${{ matrix.config.os == 'macOS-10.15' }}
       - name: Windows - Install LLVM 11.1.0
         run: choco install llvm --version=11.1.0 --allow-downgrade
         if: ${{ matrix.config.os == 'windows-2019' }}
+      - name: Get Timestamp
+        id: timestamp
+        run: Write-Host "::set-output name=timestamp::$([DateTime]::UtcNow.ToString('o'))"
+        shell: pwsh
+      - name: CCACHE cache
+        uses: actions/cache@v3
+        with:
+          path: |
+            ${{ env.CCACHE_DIR }}
+          key: ${{ matrix.config.os }}-ccache-${{ steps.timestamp.outputs.timestamp }}
+          restore-keys: |
+              ${{ matrix.config.os }}-ccache-
+        if: ${{ matrix.config.os != 'windows-2019' }}
+      - name: SCCACHE cache
+        uses: actions/cache@v3
+        with:
+          path: |
+            ${{ env.SCCACHE_DIR }}
+          key: ${{ matrix.config.os }}-sccache-${{ steps.timestamp.outputs.timestamp }}
+          restore-keys: |
+              ${{ matrix.config.os }}-sccache-
+        if: ${{ matrix.config.os == 'windows-2019' }}
       - name: "Build all"
         run: ./build.ps1
         shell: pwsh
diff --git a/.github/workflows/llvm.yml b/.github/workflows/llvm.yml
new file mode 100644
index 00000000..8ba1ed94
--- /dev/null
+++ b/.github/workflows/llvm.yml
@@ -0,0 +1,64 @@
+name: LLVM Weekly
+
+# Compile LLVM weekly or on demand
+
+on:
+  workflow_dispatch:
+  schedule:
+    - cron: '0 0 * * 0'
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  build:
+    runs-on: ${{ matrix.config.os }}
+    env: ${{ matrix.config.env }}
+    strategy:
+      fail-fast: false
+      matrix:
+        config:
+          - {
+              os: "ubuntu-20.04",
+              arch: "amd64",
+              env: {},
+            }
+          - {
+              os: "windows-2019",
+              arch: "amd64",
+              env: {},
+            }
+          - {
+              os: "macOS-10.15",
+              arch: "amd64",
+              env: {},
+            }
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: 'recursive'
+      - name: Linux - Install build dependencies, ccache, ninja
+        run: sudo apt-get install -y ccache ninja-build
+        if: ${{ matrix.config.os == 'ubuntu-20.04' }}
+      - name: Windows - Install build dependencies, sccache, ninja
+        run: choco install --accept-license -y sccache ninja
+        if: ${{ matrix.config.os == 'windows-2019' }}
+      - name: MacOS - Install build dependencies, ccache, ninja
+        run: brew install ccache ninja
+        if: ${{ matrix.config.os == 'macOS-10.15' }}
+      - name: Windows - Install LLVM 11.1.0
+        run: choco install llvm --version=11.1.0 --allow-downgrade
+        if: ${{ matrix.config.os == 'windows-2019' }}
+      - name: "Build and Package LLVM"
+        run: ./build.ps1 -t package-llvm
+        shell: pwsh
+      - name: Artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          if-no-files-found: error
+          path: |
+            target/*.zip
+            target/*.tar.gz
+            target/*.rpm
+            target/*.deb
diff --git a/.gitignore b/.gitignore
index 6edd61dc..722855d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -152,3 +152,5 @@ cython_debug/
 # Other
 /.cargo/config.toml
 wheelhouse/
+
+.local/
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 6b21389f..e69de29b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,5 +0,0 @@
-[submodule "external/llvm-project"]
-	path = external/llvm-project
-	url = https://github.com/llvm/llvm-project.git
-	branch = release/11.x
-	shallow = true
diff --git a/build.cmd b/build.cmd
index fb83b67d..e98bd0ef 100644
--- a/build.cmd
+++ b/build.cmd
@@ -1,5 +1,5 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
+@REM Copyright (c) Microsoft Corporation.
+@REM Licensed under the MIT License.
 
 @echo off
 
diff --git a/docs/development-guide/building.md b/docs/development-guide/building.md
index 59979c97..2ba35727 100644
--- a/docs/development-guide/building.md
+++ b/docs/development-guide/building.md
@@ -10,12 +10,15 @@
   (Core)](https://github.com/powershell/powershell#get-powershell)
 - [LLVM/Clang 11.1.0](https://llvm.org/) - See [Installing
   LLVM](#installing-llvm)
-- [CMake 3.20+](https://github.com/Kitware/CMake/releases/tag/v3.20.5)
-- [Ninja 1.10.0+](https://ninja-build.org/)
+- If compiling LLVM from source:
+  - [CMake 3.10+](https://github.com/Kitware/CMake/releases/tag/v3.10.3)
+  - [Ninja 1.10.0+](https://ninja-build.org/)
 
 ### Optional
 
-- [sccache](https://github.com/mozilla/sccache)
+- If building LLVM from source, either of these is highly recommended:
+  - [sccache](https://github.com/mozilla/sccache)
+  - [ccache](https://github.com/ccache/ccache)
 
 ### Linux (Ubuntu)
 
@@ -90,20 +93,6 @@ toolchain to `$HOME/.pyqir` (Windows: `$HOME\.pyqir`) and configures Rust to use
 this installation by setting the `LLVM_SYS_110_PREFIX` environment variable in
 the root `.cargo/config.toml`
 
-### Installing CMake
-
-- Linux (Ubuntu)
-
-Using `apt-get` will install 3.16, but compiling the QIR runtime requires 3.20. To install the latest version on Ubuntu, install directly from the CMake releases from GitHub:
-
-```bash
-curl -SsL https://github.com/Kitware/CMake/releases/download/v3.20.5/cmake-3.20.5-linux-x86_64.sh -o cmakeinstall.sh
-echo "f582e02696ceee81818dc3378531804b2213ed41c2a8bc566253d16d894cefab cmakeinstall.sh" | sha256sum -c --strict -
-chmod +x cmakeinstall.sh
-./cmakeinstall.sh --prefix=/usr/local --exclude-subdir
-rm cmakeinstall.sh
-```
-
 ## Development
 
 To initialize your local environment and build
@@ -146,31 +135,26 @@ Two targets are available for tox:
 - `python -m tox -e pack`
 - Packages all wheels in an isolated environment
 
-(building/environment-variables)=
-
 ### Environment Variables
 
-- `PYQIR_LLVM_EXTERNAL_DIR`
-- Path to where LLVM is already installed by user. Useful if you want to use
-  your own LLVM builds for testing.
-- `PYQIR_DOWNLOAD_LLVM`
-- Indicator to whether the build should download LLVM cached builds.
-- Build will download LLVM if needed unless this variable is defined and set to
-  `false`
-- `PYQIR_LLVM_BUILDS_URL`
-- Url from where LLVM builds will be downloaded.
-- Default: `https://msquantumpublic.blob.core.windows.net/llvm-builds`
-- `PYQIR_CACHE_DIR`
-- Root insallation path for LLVM builds
-- Default if not specified:
-  - Linux/Mac: `$HOME/.pyqir`
-  - Windows: `$HOME\.pyqir`
-- `LLVM_SYS_110_PREFIX`
-- Required by `llvm-sys` and will be set to the version of LLVM used for
-  configuration.
-- Version dependent and will change as LLVM is updated. (`LLVM_SYS_120_PREFIX`,
-  `LLVM_SYS_130_PREFIX`, etc)
-- Not needed if you have a working LLVM installation on the path.
+For those directly consuming `qirlib`, refer to the
+[Environment variables](https://github.com/qir-alliance/pyqir/blob/main/qirlib/README.md#environment-variables)
+section of it's README as the following constraints do not apply.
+
+The Python PyQIR projects require LLVM to already be installed prior to build.
+The PowerShell scripts will look at the the `qirlib`
+[Environment variables](https://github.com/qir-alliance/pyqir/blob/main/qirlib/README.md#environment-variables)
+and locate, build, or install LLVM as specified by the environment. The
+default order is:
+
+- Use specific LLVM installation if specified
+- Locate existing LLVM installation on `PATH`
+- Download LLVM if allowed from specified source
+- Build LLVM from source
+
+Afterward, the build configures the `LLVM_SYS_*_PREFIX` environment variable
+according to what the environment has configured. This will allow LLVM to
+be linked into the rest of the build.
 
 ### Packaging
 
diff --git a/eng/manylinux.Dockerfile b/eng/manylinux.Dockerfile
index cb4e2350..cf5128b1 100644
--- a/eng/manylinux.Dockerfile
+++ b/eng/manylinux.Dockerfile
@@ -1,44 +1,82 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-FROM quay.io/pypa/manylinux2014_x86_64 as builder
+FROM quay.io/pypa/manylinux2014_x86_64 as base-with-rust
 
-ENV PATH /root/.cargo/bin:$PATH
+ARG USERNAME=runner
+ARG USER_UID=1000
+ARG USER_GID=${USER_UID}
+ARG RUST_VERSION=1.57.0
 
-# todo, lock down version
-RUN curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+RUN groupadd --gid ${USER_GID} ${USERNAME}
+RUN useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME}
+RUN yum install -y sudo
+RUN echo ${USERNAME} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USERNAME}
+RUN chmod 0440 /etc/sudoers.d/${USERNAME}
 
-WORKDIR /tmp
+ENV RUSTUP_HOME=/usr/local/rustup \
+    CARGO_HOME=/usr/local/cargo \
+    PATH=/usr/local/cargo/bin:${PATH} \
+    RUST_VERSION=${RUST_VERSION}
+
+RUN curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --no-modify-path --profile minimal --default-toolchain ${RUST_VERSION} -y
+
+RUN chmod -R a+w ${RUSTUP_HOME} ${CARGO_HOME}; \
+    rustup --version; \
+    cargo --version; \
+    rustc --version;
+
+WORKDIR /io
+RUN chown ${USER_UID}:${USER_GID} /io
+
+FROM base-with-rust as builder
+
+ARG USERNAME=runner
+ARG USER_UID=1000
+ARG USER_GID=${USER_UID}
+
+USER $USERNAME
 
 # Temporary workaround installing beta for license/notice support
 RUN cargo install maturin --git https://github.com/PyO3/maturin --tag v0.12.12-beta.2
 
-FROM quay.io/pypa/manylinux2014_x86_64
+FROM base-with-rust
+
+ARG USERNAME=runner
+ARG USER_UID=1000
+ARG USER_GID=${USER_UID}
+
+USER $USERNAME
 
-ENV PATH /root/.cargo/bin:$PATH
 # Add all supported python versions
-ENV PATH /opt/python/cp36-cp36m/bin/:/opt/python/cp37-cp37m/bin/:/opt/python/cp38-cp38/bin/:/opt/python/cp39-cp39/bin/:$PATH
-# Otherwise `cargo new` errors
-ENV USER root
+ENV PATH /opt/python/cp36-cp36m/bin:/opt/python/cp37-cp37m/bin:/opt/python/cp38-cp38/bin:/opt/python/cp39-cp39/bin:/opt/python/cp310-cp310/bin:$PATH
 
-RUN curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
-    && python3 -m pip install --no-cache-dir cffi \
-    && mkdir /io
+RUN python3.6 -m pip install --no-cache-dir cffi \
+    && python3.7 -m pip install --no-cache-dir cffi \
+    && python3.8 -m pip install --no-cache-dir cffi \
+    && python3.9 -m pip install --no-cache-dir cffi \
+    && python3.10 -m pip install --no-cache-dir cffi
 
-COPY --from=builder /root/.cargo/bin/maturin /usr/bin/maturin
+COPY --from=builder ${CARGO_HOME}/bin/maturin /usr/bin/maturin
 
-WORKDIR /io
+USER root
+
+RUN yum install -y libffi-devel ninja-build ccache
+
+ADD https://repo.anaconda.com/miniconda/Miniconda3-py39_4.10.3-Linux-x86_64.sh /tmp/Miniconda3.sh
+
+RUN /bin/bash /tmp/Miniconda3.sh -b -p /usr/local/miniconda3
 
-RUN yum install -y libffi-devel ninja-build
+RUN chown -R ${USER_UID}:${USER_GID} /usr/local/miniconda3
 
-ADD https://repo.anaconda.com/miniconda/Miniconda3-py39_4.10.3-Linux-x86_64.sh /tmp/Minoconda.sh
+USER $USERNAME
 
-RUN /bin/bash /tmp/Minoconda.sh -b
+ENV PATH="/usr/local/miniconda3/bin:${PATH}"
 
-ENV PATH="/root/miniconda3/bin:${PATH}"
+ENV PATH="/usr/lib/ccache:${PATH}"
 
 RUN conda init && \
     conda install -y -c conda-forge clang-11 libstdcxx-devel_linux-64 libgcc-devel_linux-64 && \
-    cp /root/miniconda3/bin/clang-11 /root/miniconda3/bin/clang++-11
+    cp /usr/local/miniconda3/bin/clang-11 /usr/local/miniconda3/bin/clang++-11
 
-RUN python -m pip install -U tox
+RUN conda run python -m pip install -U tox
diff --git a/eng/psakefile.ps1 b/eng/psakefile.ps1
index d14d04d3..fa341e77 100644
--- a/eng/psakefile.ps1
+++ b/eng/psakefile.ps1
@@ -8,6 +8,10 @@ properties {
 
     $pyqir = @{}
 
+    $pyqir.qirlib = @{}
+    $pyqir.qirlib.name = "qirlib"
+    $pyqir.qirlib.dir = Join-Path $repo.root "qirlib"
+
     $pyqir.meta = @{}
     $pyqir.meta.name = "pyqir"
     $pyqir.meta.dir = Join-Path $repo.root "pyqir"
@@ -42,6 +46,8 @@ Task default -Depends checks, pyqir-tests, parser, generator, evaluator, metawhe
 
 Task checks -Depends cargo-fmt, cargo-clippy
 
+Task rebuild -Depends generator, evaluator, parser
+
 Task cargo-fmt {
     Invoke-LoggedCommand -workingDirectory $repo.root -errorMessage "Please run 'cargo fmt --all' before pushing" {
         cargo fmt --all -- --check
@@ -51,17 +57,8 @@ Task cargo-fmt {
 Task cargo-clippy -Depends init {
     Invoke-LoggedCommand -workingDirectory $repo.root -errorMessage "Please fix the above clippy errors" {
         $extraArgs = (Test-CI) ? @("--", "-D", "warnings") : @()
-        cargo clippy --workspace --all-targets --all-features @extraArgs
-    }
-}
-
-Task init {
-    if ((Test-CI) -and !$IsLinux) {
-        cargo install maturin --git https://github.com/PyO3/maturin --tag v0.12.12-beta.2
+        cargo clippy --workspace --all-targets @extraArgs
     }
-    Restore-ConfigTomlWithLlvmInfo
-    Test-Prerequisites
-    Initialize-Environment
 }
 
 Task generator -Depends init {
@@ -77,9 +74,31 @@ Task parser -Depends init {
 }
 
 Task pyqir-tests -Depends init {
-    Invoke-LoggedCommand -workingDirectory (Join-Path $repo.root pyqir-tests) {
-        & $python -m pip install tox
-        & $python -m tox -e test
+    $srcPath = $repo.root
+    $project = "pyqir-tests"
+    $installationDirectory = Resolve-InstallationDirectory
+
+    if (Test-RunInContainer) {
+        Build-ContainerImage $srcPath
+        Write-BuildLog "Running container image:"
+        $ioVolume = "$($srcPath):/io"
+        $llvmVolume = "$($installationDirectory):/usr/lib/llvm"
+        $userName = [Environment]::UserName
+
+        Invoke-LoggedCommand {
+            docker run --rm --user $userName -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output python -m tox -e test
+        }
+    }
+    else {
+        exec -workingDirectory (Join-Path $srcPath $project) {
+            Invoke-LoggedCommand {
+                & $python -m pip install tox
+            }
+
+            Invoke-LoggedCommand {
+                & $python -m tox -e test
+            }
+        }
     }
 }
 
@@ -93,7 +112,6 @@ Task metawheel {
     }
 }
 
-Task rebuild -Depends generator, evaluator, parser
 Task wheelhouse `
     -Precondition { -not (Test-Path $wheelhouse -ErrorAction SilentlyContinue) } `
 { Invoke-Task rebuild }
@@ -117,166 +135,170 @@ Task docs -Depends wheelhouse {
 }
 
 function Use-ExternalLlvmInstallation {
-    Write-BuildLog "Using LLVM installation specified by PYQIR_LLVM_EXTERNAL_DIR"
-    Assert (Test-Path $env:PYQIR_LLVM_EXTERNAL_DIR) "PYQIR_LLVM_EXTERNAL_DIR folder does not exist"
-    Use-LlvmInstallation $env:PYQIR_LLVM_EXTERNAL_DIR
+    Write-BuildLog "Using LLVM installation specified by QIRLIB_LLVM_EXTERNAL_DIR"
+    Assert (Test-Path $env:QIRLIB_LLVM_EXTERNAL_DIR) "QIRLIB_LLVM_EXTERNAL_DIR folder does not exist"
+    Use-LlvmInstallation $env:QIRLIB_LLVM_EXTERNAL_DIR
 }
 
 function Test-AllowedToDownloadLlvm {
-    # If PYQIR_DOWNLOAD_LLVM isn't set, we allow for download
+    # If QIRLIB_DOWNLOAD_LLVM isn't set, we allow for download
     # If it is set, then we use its value
-    !((Test-Path env:\PYQIR_DOWNLOAD_LLVM) -and ($env:PYQIR_DOWNLOAD_LLVM -eq $false))
+    !((Test-Path env:\QIRLIB_DOWNLOAD_LLVM) -and ($env:QIRLIB_DOWNLOAD_LLVM -eq $false))
 }
 
-function Get-LlvmDownloadBaseUrl {
-    if (Test-Path env:\PYQIR_LLVM_BUILDS_URL) {
-        $env:PYQIR_LLVM_BUILDS_URL
+task init {
+    # Temorary, install maturin v0.12.12-beta.2 which has the
+    # PEP 639 license fixes.
+    if ((Test-CI) -and !$IsLinux) {
+        cargo install maturin --git https://github.com/PyO3/maturin --tag v0.12.12-beta.2
     }
-    else {
-        "https://github.com/qir-alliance/pyqir/releases/download/qirlib-llvmorg-11.1.0"
+    
+    # qirlib has this logic built in when compiled on its own
+    # but we must have LLVM installed prior to the wheels being built.
+    
+    # if an external LLVM is specified, make sure it exist and
+    # skip further bootstapping
+    if (Test-Path env:\QIRLIB_LLVM_EXTERNAL_DIR) {
+        Use-ExternalLlvmInstallation
     }
-}
-
-function Get-PackageExt {
-    $extension = ".tar.gz"
-    if ($IsWindows) {
-        $extension = ".zip"
+    else {
+        $packagePath = Resolve-InstallationDirectory
+        if (Test-Path (Join-Path $packagePath "bin")) {
+            Write-BuildLog "LLVM target is already installed."
+            # LLVM is already downloaded
+            Use-LlvmInstallation $packagePath
+        }
+        else {
+            Write-BuildLog "LLVM target is not installed."
+            if (Test-AllowedToDownloadLlvm) {
+                Write-BuildLog "Downloading LLVM target"
+                Invoke-Task "install-llvm-from-archive"
+            }
+            else {
+                Write-BuildLog "Downloading LLVM Disabled, building from source."
+                # We don't have an external LLVM installation specified
+                # We are not downloading LLVM
+                # So we need to build it.
+                Invoke-Task "install-llvm-from-source"
+            }
+        }
     }
-    $extension
 }
 
-function Get-LlvmArchiveUrl {
-    $extension = Get-PackageExt
-    $packageName = Get-PackageName
-    $baseUrl = Get-LlvmDownloadBaseUrl
-    "$baseUrl/$($packageName)$extension"
+task install-llvm-from-archive {
+    install-llvm $pyqir.qirlib.dir "download"
 }
 
-function Get-LlvmArchiveShaUrl {
-    $extension = Get-PackageExt
-    $packageName = Get-PackageName
-    $baseUrl = Get-LlvmDownloadBaseUrl
-    "$baseUrl/$($packageName)$extension.sha256"
-}
-
-function Get-LlvmArchiveFileName {
-    $packageName = Get-PackageName
-    $extension = Get-PackageExt
-    "$($packageName)$extension"
-}
-
-function Get-LlvmArchiveShaFileName {
-    $filename = Get-LlvmArchiveFileName
-    "$filename.sha256"
-}
-
-function Install-LlvmFromBuildArtifacts {
-    [CmdletBinding()]
-    param (
-        [Parameter()]
-        [string]
-        $packagePath
-    )
-
-    $outFile = Join-Path $($env:TEMP) (Get-LlvmArchiveFileName)
-    if ((Test-Path $outFile)) {
-        Remove-Item $outFile
+function Write-CacheStats {
+    if (Test-CommandExists("ccache")) {
+        Write-BuildLog "ccache config:"
+        & { ccache --show-config } -ErrorAction SilentlyContinue
+        Write-BuildLog "ccache stats:"
+        & { ccache --show-stats } -ErrorAction SilentlyContinue
     }
-
-    $archiveUrl = Get-LlvmArchiveUrl
-    Write-BuildLog "Dowloading $archiveUrl to $outFile"
-    Invoke-WebRequest -Uri $archiveUrl -OutFile $outFile
-
-    $shaFile = Join-Path $($env:TEMP) (Get-LlvmArchiveShaFileName)
-    if ((Test-Path $shaFile)) {
-        Remove-Item $shaFile
+    if (Test-CommandExists("sccache")) {
+        Write-BuildLog "sccache config/stats:"
+        & { sccache --show-stats } -ErrorAction SilentlyContinue
     }
+}
 
-    $sha256Url = Get-LlvmArchiveShaUrl
-    Write-BuildLog "Dowloading $sha256Url to $shaFile"
-    Invoke-WebRequest -Uri $sha256Url -OutFile $shaFile
-    Write-BuildLog "Calculating hash for $outFile"
-    $calculatedHash = (Get-FileHash -Path $outFile -Algorithm SHA256).Hash
-
-    Write-BuildLog "Reading hash from $shaFile"
-    $expectedHash = (Get-Content -Path $shaFile)
-
-    Assert ("$calculatedHash" -eq "$expectedHash") "The calculated hash $calculatedHash did not match the expected hash $expectedHash"
+task install-llvm-from-source {
+    Write-CacheStats
+    if (Test-CommandExists("sccache")) {
+        Write-BuildLog "Starting sccache server"
+        & { sccache --start-server } -ErrorAction SilentlyContinue
+        Write-BuildLog "Started sccache server"
+    }
 
-    $packagesRoot = Get-AqCacheDirectory
-    if ($IsWindows) {
-        Expand-Archive -Path $outFile -DestinationPath $packagesRoot
+    if (Test-RunInContainer) {
+        $installationDirectory = Resolve-InstallationDirectory
+        Use-LlvmInstallation $installationDirectory
+        Build-ContainerImage $repo.root
+        $srcPath = $repo.root
+
+        # For any of the volumes mapped, if the dir doesn't exist,
+        # docker will create it and it will be owned by root and
+        # the caching/install breaks with permission errors.
+        # New-Item is idempotent so we don't need to check for existence
+
+        $ioVolume = "$($srcPath):/io"
+        $llvmVolume = "$($installationDirectory):/llvm"
+        New-Item -ItemType Directory -Force $installationDirectory | Out-Null
+        
+        $userName = [Environment]::UserName
+        $cacheMount = ""
+        $cacheEnv = ""
+        # only ccache is supported in the manylinux container for now.
+        # we would need a way to specify which cache is used to
+        # support both.
+        if (Test-CommandExists("ccache")) {
+            # we need to map the local cache dir into the
+            # container. If the env var isn't set, ask ccache
+            $cacheDir = ""
+            if (Test-Path env:\CCACHE_DIR) {
+                $cacheDir = $Env:CCACHE_DIR
+            }
+            else {
+                $cacheDir = exec { ccache -k cache_dir }
+            }
+            if (![string]::IsNullOrWhiteSpace($cacheDir)) {
+                New-Item -ItemType Directory -Force $cacheDir | Out-Null
+                
+                $cacheDir = Resolve-Path $cacheDir
+                # mount the cache outside of any runner mappings
+                $cacheMount = @("-v", "$($cacheDir):/ccache")
+                $cacheEnv = @("-e", "CCACHE_DIR=`"/ccache`"")
+            }
+        }
+        
+        Invoke-LoggedCommand {
+            docker run --rm --user $userName -v $ioVolume -v $llvmVolume @cacheMount @cacheEnv -e QIRLIB_CACHE_DIR="$installationDirectory" -e QIRLIB_CACHE_DIR="/llvm" -w /io/qirlib manylinux2014_x86_64_maturin conda run --no-capture-output cargo build --release --no-default-features --features "build-llvm,no-llvm-linking" -vv
+        }
     }
     else {
-        tar -zxvf $outFile -C $packagesRoot
+        if ($IsWindows) {
+            Include vcvars.ps1
+        }
+        
+        install-llvm $pyqir.qirlib.dir "build"
     }
 
-    $packageName = Get-PackageName
-    $packagePath = Get-InstallationDirectory $packageName
-    Use-LlvmInstallation $packagePath
+    Write-CacheStats
 }
 
-function Install-LlvmFromSource {
-    [CmdletBinding()]
-    param (
-        [Parameter()]
-        [string]
-        $packagePath
-    )
-    $Env:PKG_NAME = Get-PackageName
-    $Env:CMAKE_INSTALL_PREFIX = $packagePath
-    $Env:INSTALL_LLVM_PACKAGE = $true
-    Assert $false -failureMessage "TODO: Migration in progress"
-    . (Join-Path (Get-RepoRoot) "build" "llvm.ps1")
-    Use-LlvmInstallation $packagePath
-}
-
-function Test-Prerequisites {
-    if (!(Test-LlvmSubmoduleInitialized)) {
-        Write-BuildLog "llvm-project submodule isn't initialized"
-        Write-BuildLog "Initializing submodules: git submodule init"
-        exec -workingDirectory ($repo.root ) { git submodule init }
-        Write-BuildLog "Updating submodules: git submodule update --depth 1 --recursive"
-        exec -workingDirectory ($repo.root ) { git submodule update --depth 1 --recursive }
-    }
-    Assert (Test-LlvmSubmoduleInitialized) "Failed to read initialized llvm-project submodule"
-}
+task package-llvm {
+    if (Test-RunInContainer) {
+        Build-ContainerImage $repo.root
+        $srcPath = $repo.root
+        $ioVolume = "$($srcPath):/io"
+        $userName = [Environment]::UserName
 
-function Initialize-Environment {
-    # if an external LLVM is specified, make sure it exist and
-    # skip further bootstapping
-    if (Test-Path env:\PYQIR_LLVM_EXTERNAL_DIR) {
-        Use-ExternalLlvmInstallation
+        Invoke-LoggedCommand {
+            docker run --rm --user $userName -v $ioVolume -w /io/qirlib -e QIRLIB_PKG_DEST=/io/target manylinux2014_x86_64_maturin conda run --no-capture-output cargo build --release  --no-default-features --features package-llvm -vv
+        }
     }
     else {
-        $llvmTag = Get-LlvmTag
-        Write-BuildLog "llvm-project tag: $llvmTag"
-        $packageName = Get-PackageName
-
-        $packagePath = Get-InstallationDirectory $packageName
-        if (Test-Path $packagePath) {
-            Write-BuildLog "LLVM target $($llvmTag) is already installed."
-            # LLVM is already downloaded
-            Use-LlvmInstallation $packagePath
+        if ($IsWindows) {
+            Include vcvars.ps1
         }
-        else {
-            Write-BuildLog "LLVM target $($llvmTag) is not installed."
-            if (Test-AllowedToDownloadLlvm) {
-                Write-BuildLog "Downloading LLVM target $packageName "
-                Install-LlvmFromBuildArtifacts $packagePath
+        $clear_pkg_dest_var = $false
+        if (!(Test-Path env:\QIRLIB_PKG_DEST)) {
+            $clear_pkg_dest_var = $true
+            $env:QIRLIB_PKG_DEST = Join-Path $repo.root "target"
+        }
+        try {
+            Invoke-LoggedCommand -wd $pyqir.qirlib.dir {
+                cargo build --release  --no-default-features --features package-llvm -vv
             }
-            else {
-                Write-BuildLog "Downloading LLVM Disabled, building from source."
-                # We don't have an external LLVM installation specified
-                # We are not downloading LLVM
-                # So we need to build it.
-                Install-LlvmFromSource $packagePath
+        }
+        finally {
+            if ($clear_pkg_dest_var) {
+                Remove-Item -Path Env:QIRLIB_PKG_DEST
             }
         }
     }
 }
 
-
 # Only run the nested ManyLinux container
 # build on Linux while not in a dev container
 function Test-RunInContainer {
@@ -291,41 +313,52 @@ function Test-RunInContainer {
     }
 }
 
+function Build-ContainerImage([string]$srcPath) {
+    Write-BuildLog "Building container image manylinux-llvm-builder"
+    Invoke-LoggedCommand -workingDirectory (Join-Path $srcPath eng) {
+        $user = [environment]::UserName
+        $uid = "$(id -u)"
+        $gid = "$(id -g)"
+        $rustv = "1.57.0"
+        $tag = "manylinux2014_x86_64_maturin"
+        Get-Content manylinux.Dockerfile | docker build `
+            --build-arg USERNAME=$user `
+            --build-arg USER_UID=$uid `
+            --build-arg USER_GID=$gid `
+            --build-arg RUST_VERSION=$rustv `
+            -t $tag -
+    }
+}
+
 function Build-PyQIR([string]$project) {
     $srcPath = $repo.root
     $installationDirectory = Resolve-InstallationDirectory
 
     if (Test-RunInContainer) {
-        function Build-ContainerImage {
-            Write-BuildLog "Building container image manylinux-llvm-builder"
-            Invoke-LoggedCommand -workingDirectory (Join-Path $srcPath eng) {
-                Get-Content manylinux.Dockerfile | docker build -t manylinux2014_x86_64_maturin -
-            }
-        }
+        Build-ContainerImage $srcPath
         function Invoke-ContainerImage {
             Write-BuildLog "Running container image:"
             $ioVolume = "$($srcPath):/io"
             $llvmVolume = "$($installationDirectory):/usr/lib/llvm"
-            $userSpec = ""
+            $userName = [Environment]::UserName
 
             Invoke-LoggedCommand {
-                docker run --rm $userSpec -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output cargo test --release --lib -vv -- --nocapture
+                docker run --rm --user $userName -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output cargo test --release --lib -vv -- --nocapture
             }
 
             Invoke-LoggedCommand {
-                docker run --rm $userSpec -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output /usr/bin/maturin build --release
+                docker run --rm --user $userName -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output /usr/bin/maturin build --release
             }
 
             Invoke-LoggedCommand {
-                docker run --rm $userSpec -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output python -m tox -e test
+                docker run --rm --user $userName -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output python -m tox -e test
             }
             
             Invoke-LoggedCommand {
-                docker run --rm $userSpec -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output cargo test --release -vv -- --nocapture
+                docker run --rm --user $userName -v $ioVolume -v $llvmVolume -e LLVM_SYS_110_PREFIX=/usr/lib/llvm -w /io/$project manylinux2014_x86_64_maturin conda run --no-capture-output cargo test --release -vv -- --nocapture
             }
         }
 
-        Build-ContainerImage
         Invoke-ContainerImage
     }
     else {
@@ -350,7 +383,7 @@ function Build-PyQIR([string]$project) {
     }
 }
 
-task run-examples-in-containers -precondition { $IsLinux -and (Test-CI) } {
+task run-examples-in-containers -precondition { Test-RunInContainer } {
     $userName = [Environment]::UserName
     $userId = $(id -u)
     $groupId = $(id -g)
@@ -431,6 +464,34 @@ function Create-DocsEnv() {
     }
 }
 
+function install-llvm {
+    Param(
+        [Parameter(Mandatory)]
+        [string]$qirlibDir,
+        [Parameter(Mandatory)]
+        [ValidateSet("download", "build")]
+        [string]$operation
+    )
+
+    $installationDirectory = Resolve-InstallationDirectory
+    Use-LlvmInstallation $installationDirectory
+    $clear_cache_var = $false
+    if (!(Test-Path env:\QIRLIB_CACHE_DIR)) {
+        $clear_cache_var = $true
+        $env:QIRLIB_CACHE_DIR = $installationDirectory
+    }
+    try {
+        Invoke-LoggedCommand -wd $qirlibDir {
+            cargo build --release --no-default-features --features "$($operation)-llvm,no-llvm-linking" -vv
+        }
+    }
+    finally {
+        if ($clear_cache_var) {
+            Remove-Item -Path Env:QIRLIB_CACHE_DIR
+        }
+    }   
+}
+
 task check-licenses {
     # Uses cargo-deny to verify that the linked components
     # only use approved licenses
diff --git a/eng/utils.ps1 b/eng/utils.ps1
index f37c7fdc..d3aca521 100644
--- a/eng/utils.ps1
+++ b/eng/utils.ps1
@@ -55,97 +55,6 @@ function Test-InDevContainer {
     $IsLinux -and (Test-Path env:\IN_DEV_CONTAINER)
 }
 
-# Updates the cargo package version with the version specified.
-function Restore-CargoTomlWithVersionInfo ($inputFile, $outputFile, $version) {
-    $outFile = New-Item -ItemType File -Path $outputFile
-    $inPackageSection = $false
-    switch -regex -file $inputFile {
-        "^\[(.+)\]" {
-            # Section
-            $section = $matches[1]
-            $inPackageSection = $section -eq "package"
-            Add-Content -Path $outFile -Value $_
-        }
-        "(.+?)\s*=(.*)" {
-            # Key/Value
-            $key, $value = $matches[1..2]
-            if ($inPackageSection -and ($key -eq "version")) {
-                $value = "version = ""$($version)"""
-                Add-Content -Path $outFile -Value $value
-            }
-            else {
-                Add-Content -Path $outFile -Value $_
-            }
-        }
-        default {
-            Add-Content -Path $outFile -Value $_
-        }
-    }
-}
-
-# Copies the default config.toml and sets the [env] config
-# section to specify the variables needed for llvm-sys/inkwell
-# This allows us to not need the user to specify env vars to build.
-function Restore-ConfigTomlWithLlvmInfo {
-    $cargoPath = Resolve-Path (Join-Path (Get-RepoRoot) '.cargo')
-    $configTemplatePath = Join-Path $cargoPath config.toml.template
-    $configPath = Join-Path $cargoPath config.toml
-
-    # remove the old file if it exists.
-    if (Test-Path $configPath) {
-        Remove-Item $configPath
-    }
-
-    # ensure the output folder is there, `mkdir -p` equivalent
-    New-Item -ItemType Directory -Path $cargoPath -Force | Out-Null
-
-    # copy the template
-    Copy-Item $configTemplatePath $configPath
-
-    # append the env vars to the new config
-    $installationDirectory = Resolve-InstallationDirectory
-    Add-Content -Path $configPath -Value "[env]"
-    Add-Content -Path $configPath -Value "LLVM_SYS_110_PREFIX = '$installationDirectory'"
-}
-
-function Get-LlvmSubmoduleSha {
-    $status = Get-LlvmSubmoduleStatus
-    $sha = $status.Substring(1, 9)
-    $sha
-}
-
-function Get-LlvmSubmoduleStatus {
-    $statusResult = exec -workingDirectory (Get-RepoRoot) { git submodule status --cached }
-    # on all platforms, the status uses '/' in the module path.
-    $status = $statusResult.Split([Environment]::NewLine) | Where-Object { $_.Contains("external/llvm-project") } | Select-Object -First 1
-    $status
-}
-
-function Test-LlvmSubmoduleInitialized {
-    $status = Get-LlvmSubmoduleStatus
-    if ($status.Substring(0, 1) -eq "-") {
-        return $false
-    }
-    else {
-        return $true
-    }
-}
-
-# Gets the LLVM package triple for the current platform
-function Get-TargetTriple {
-    $triple = "unknown"
-    if ($IsWindows) {
-        $triple = "x86_64-pc-windows-msvc-static"
-    }
-    elseif ($IsLinux) {
-        $triple = "x86_64-unknown-linux-gnu"
-    }
-    elseif ($IsMacOS) {
-        $triple = "x86_64-apple-darwin"
-    }
-    $triple
-}
-
 # This method should be able to be removed when Rust 1.56 is released
 # which contains the feature for env sections in the .cargo/config.toml
 function Use-LlvmInstallation {
@@ -156,56 +65,22 @@ function Use-LlvmInstallation {
     $env:LLVM_SYS_110_PREFIX = $path
 }
 
-# Gets the LLVM version git hash or tag name
-function Get-LlvmTag {
-    "llvmorg-11.1.0"
-}
-
-function Get-PackageName {
-    $tag = Get-LlvmTag
-    $TARGET_TRIPLE = Get-TargetTriple
-    $packageName = "qirlib-llvm-$($TARGET_TRIPLE)-$($tag)"
-    $packageName
-}
-
-function Get-DefaultInstallDirectory {
-    if (Test-Path env:\PYQIR_CACHE_DIR) {
-        $env:PYQIR_CACHE_DIR
+function Resolve-InstallationDirectory {
+    if (Test-Path env:\QIRLIB_LLVM_EXTERNAL_DIR) {
+        return $env:QIRLIB_LLVM_EXTERNAL_DIR
     }
     else {
-        Join-Path "$HOME" ".pyqir"
-    }
-}
-
-function Get-AqCacheDirectory {
-    $aqCacheDirectory = (Get-DefaultInstallDirectory)
-    if (!(Test-Path $aqCacheDirectory)) {
-        mkdir $aqCacheDirectory | Out-Null
+        $packagePath = Get-DefaultInstallDirectory
+        return $packagePath
     }
-    Resolve-Path $aqCacheDirectory
-}
-
-function Get-InstallationDirectory {
-    [CmdletBinding()]
-    param (
-        [Parameter()]
-        [string]
-        $packageName
-    )
-    $aqCacheDirectory = Get-AqCacheDirectory
-    $packagePath = Join-Path $aqCacheDirectory $packageName
-    $packagePath
 }
 
-function Resolve-InstallationDirectory {
-    if (Test-Path env:\PYQIR_LLVM_EXTERNAL_DIR) {
-        return $env:PYQIR_LLVM_EXTERNAL_DIR
+function Get-DefaultInstallDirectory {
+    if (Test-Path env:\QIRLIB_CACHE_DIR) {
+        $env:QIRLIB_CACHE_DIR
     }
     else {
-        $packageName = Get-PackageName
-
-        $packagePath = Get-InstallationDirectory $packageName
-        return $packagePath
+        Join-Path "$HOME" ".pyqir"
     }
 }
 
@@ -227,7 +102,9 @@ function Invoke-LoggedCommand {
         [Alias("wd")]
         [string]$workingDirectory = $null
     )
-    Write-BuildLog "$cmd".Trim() "command"
+
+    Write-BuildLog "Invoke-LoggedCommand in $workingDirectory`:"
+    Write-BuildLog $ExecutionContext.InvokeCommand.ExpandString($cmd).Trim() "command"
 
     # errorMessage pulls default values from psake. We
     # only want to pass the param if we want to override.
diff --git a/eng/vcvars.ps1 b/eng/vcvars.ps1
new file mode 100644
index 00000000..8b926fc9
--- /dev/null
+++ b/eng/vcvars.ps1
@@ -0,0 +1,20 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+if ($IsWindows) {
+    # find VS root
+    $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
+    $visualStudioPath = & $vswhere -prerelease -latest -property installationPath
+    Write-Output "vs located at: $visualStudioPath"
+
+    # Call vcvars64.bat and write the set calls to file
+    cmd.exe /c "call `"$visualStudioPath\VC\Auxiliary\Build\vcvars64.bat`" && set > %temp%\vcvars.txt"
+
+    # Read the set calls and set the corresponding pwsh env vars
+    Get-Content "$Env:temp\vcvars.txt" | Foreach-Object {
+        if ($_ -match "^(.*?)=(.*)$") {
+            Set-Content "env:\$($matches[1])" $matches[2]
+            Write-Host "setting env: $($matches[1]) = $($matches[2])"
+        }
+    }
+}
\ No newline at end of file
diff --git a/examples/generator/requirements.txt b/examples/generator/requirements.txt
index 730d1923..f796e2a0 100644
--- a/examples/generator/requirements.txt
+++ b/examples/generator/requirements.txt
@@ -1,1 +1,1 @@
-antlr4-python3-runtime
\ No newline at end of file
+antlr4-python3-runtime==4.9.3
\ No newline at end of file
diff --git a/external/llvm-project b/external/llvm-project
deleted file mode 160000
index 1fdec59b..00000000
--- a/external/llvm-project
+++ /dev/null
@@ -1,1 +0,0 @@
-Subproject commit 1fdec59bffc11ae37eb51a1b9869f0696bfd5312
diff --git a/pyqir-evaluator/Cargo.toml b/pyqir-evaluator/Cargo.toml
index bb8d633a..ff46f382 100644
--- a/pyqir-evaluator/Cargo.toml
+++ b/pyqir-evaluator/Cargo.toml
@@ -13,7 +13,6 @@ repository = "https://github.com/qir-alliance/pyqir"
 
 [dependencies]
 qirlib = { path = "../qirlib" }
-inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", default-features = false, features = ["llvm11-0", "target-x86"] }
 
 [dependencies.pyo3]
 version = "0.15.1"
diff --git a/pyqir-generator/Cargo.toml b/pyqir-generator/Cargo.toml
index 49cabf70..42359bbe 100644
--- a/pyqir-generator/Cargo.toml
+++ b/pyqir-generator/Cargo.toml
@@ -13,7 +13,6 @@ repository = "https://github.com/qir-alliance/pyqir"
 
 [dependencies]
 env_logger = "0.9.0"
-inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", default-features = false, features = ["llvm11-0", "target-x86"] }
 log = "0.4.14"
 pyo3 = "0.15.1"
 qirlib = { path = "../qirlib" }
diff --git a/qirlib/CMakeLists.txt b/qirlib/CMakeLists.txt
new file mode 100644
index 00000000..1788a53d
--- /dev/null
+++ b/qirlib/CMakeLists.txt
@@ -0,0 +1,84 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+cmake_minimum_required(VERSION 3.10.0)
+
+project(qirlib_external)
+include(ExternalProject)
+
+# Environment variables
+# Download vars
+# - QIRLIB_DOWNLOAD_LLVM => If set, download pre-built LLVM binaries.
+# - QIRLIB_LLVM_BUILDS_URL => URL root where downloads are hosted.
+# - QIRLIB_LLVM_PKG_NAME => name of package to be downloaded/created
+# - CPACK_PACKAGE_FILE_NAME
+# Build vars
+# - QIRLIB_LLVM_TAG
+# Both
+# - CMAKE_INSTALL_PREFIX
+
+if (NOT $ENV{QIRLIB_DOWNLOAD_LLVM} MATCHES "false")
+  message (STATUS "Install LLVM to ${CMAKE_INSTALL_PREFIX}")
+
+  set(SHA256_NAME $ENV{QIRLIB_LLVM_PKG_NAME}.sha256)
+  message (STATUS "Download file: $ENV{QIRLIB_LLVM_BUILDS_URL}/$ENV{QIRLIB_LLVM_PKG_NAME}")
+  message (STATUS "Download sha:$ENV{QIRLIB_LLVM_BUILDS_URL}/${SHA256_NAME}")
+
+  # download the expected archive hash
+  file(DOWNLOAD "$ENV{QIRLIB_LLVM_BUILDS_URL}/${SHA256_NAME}" ${CMAKE_BINARY_DIR}/${SHA256_NAME} STATUS SHA256_DOWNLOAD_STATUS)
+  list(GET SHA256_DOWNLOAD_STATUS 0 SHA256_DOWNLOAD_STATUS)
+  if (NOT (${SHA256_DOWNLOAD_STATUS} EQUAL 0))
+    list(GET SHA256_DOWNLOAD_STATUS 1 ERROR_MESSAGE)
+    message(FATAL_ERROR "Failed to download sha256 hash: ${ERROR_MESSAGE}")
+  endif ()
+
+  # load the expected archive hash into a variable for cmake to check
+  file(STRINGS ${CMAKE_BINARY_DIR}/${SHA256_NAME} LLVM_SHA256_SUM REGEX [0-9a-fA-F]+)
+  string(STRIP ${LLVM_SHA256_SUM} LLVM_SHA256_SUM)
+
+  ExternalProject_Add(llvm
+    URL $ENV{QIRLIB_LLVM_BUILDS_URL}/$ENV{QIRLIB_LLVM_PKG_NAME}
+    URL_HASH SHA256=${LLVM_SHA256_SUM}
+    CONFIGURE_COMMAND ""
+    SOURCE_DIR ${CMAKE_INSTALL_PREFIX}
+    BUILD_COMMAND ""
+    INSTALL_COMMAND ""
+    TEST_COMMAND ""
+    USES_TERMINAL_DOWNLOAD  TRUE
+  )
+else()
+  # The outer cmake call from rust will configure CMAKE_INSTALL_PREFIX
+  # Unless the user overrides the install path rust will configure it to 
+  # be the crate OUT_DIR.
+  # The build will define CPACK_PACKAGE_FILE_NAME passing it to the LLVM build.
+  set (EXTERNAL_EXTRA_CMAKE_ARGS)
+  list (APPEND EXTERNAL_EXTRA_CMAKE_ARGS 
+    -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}
+    -DCPACK_PACKAGE_FILE_NAME=${CPACK_PACKAGE_FILE_NAME}
+  )
+
+  ExternalProject_Add(llvm
+    GIT_REPOSITORY    https://github.com/llvm/llvm-project.git
+    GIT_TAG           $ENV{QIRLIB_LLVM_TAG}
+    GIT_SHALLOW       TRUE
+    GIT_PROGRESS      TRUE
+    SOURCE_SUBDIR     llvm
+    CONFIGURE_COMMAND ${CMAKE_COMMAND} -G Ninja -C ${CMAKE_CURRENT_LIST_DIR}/config.cmake ${EXTERNAL_EXTRA_CMAKE_ARGS} <SOURCE_DIR><SOURCE_SUBDIR>
+    BUILD_COMMAND     ninja
+    USES_TERMINAL_DOWNLOAD  TRUE
+    USES_TERMINAL_CONFIGURE TRUE
+    USES_TERMINAL_BUILD     TRUE
+    USES_TERMINAL_INSTALL   TRUE
+  )
+
+  ExternalProject_Add_Step(llvm package
+    COMMAND           ninja package
+    WORKING_DIRECTORY <BINARY_DIR>
+    COMMENT           "package llvm"
+    DEPENDEES         build configure
+    ALWAYS            TRUE
+    EXCLUDE_FROM_MAIN TRUE
+    USES_TERMINAL     TRUE
+  )
+  ExternalProject_Add_StepTargets(llvm package)
+endif()
\ No newline at end of file
diff --git a/qirlib/Cargo.toml b/qirlib/Cargo.toml
index c0c617e9..e7414932 100644
--- a/qirlib/Cargo.toml
+++ b/qirlib/Cargo.toml
@@ -1,16 +1,21 @@
 [package]
 name = "qirlib"
 version = "0.3.0"
-license = "MIT"
 edition = "2018"
+license = "MIT"
 description = "Base Profile QIR library"
+build = "build.rs"
+readme = "README.md"
+homepage = "https://github.com/qir-alliance/pyqir"
+repository = "https://github.com/qir-alliance/pyqir"
+rust-version = "1.56"
 
 [dependencies]
 llvm-sys = "110"
 inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", default-features = false, features = ["llvm11-0", "target-x86"] }
 log = "0.4.14"
-cty="0.2.1"
-rand="0.8.4"
+cty = "0.2.1"
+rand = "0.8.4"
 base64 = "0.13.0"
 lazy_static = "1.4.0"
 mut_static = "5.0.0"
@@ -21,8 +26,30 @@ serial_test = "0.5.1"
 tempfile = "3.2.0"
 normalize-line-endings = "0.3.0"
 
+[build-dependencies]
+cmake = "0.1.48"
+cc = "1.0"
+lazy_static = "1.0"
+regex = "1.0"
+semver = "1.0.6"
+
 [lib]
 name = "qirlib"
 
 [features]
-default = []
\ No newline at end of file
+
+# default to use llvm-sys/inkwell for llvm linking
+default = ["external-llvm-linking"]
+external-llvm-linking = []
+# disable linking for local installation or packaging
+no-llvm-linking = ["llvm-sys/disable-alltargets-init", "inkwell/llvm11-0-no-llvm-linking"]
+# let qirlib do the llvm linking
+qirlib-llvm-linking = ["llvm-sys/disable-alltargets-init", "inkwell/llvm11-0-no-llvm-linking"]
+
+download-llvm = []
+build-llvm = []
+
+# Dev use only for packaging LLVM builds
+package-llvm = ["build-llvm", "no-llvm-linking"]
+
+
diff --git a/qirlib/NOTICE.txt b/qirlib/NOTICE.txt
new file mode 100644
index 00000000..d62f2cf8
--- /dev/null
+++ b/qirlib/NOTICE.txt
@@ -0,0 +1,57 @@
+THIRD PARTY SOFTWARE NOTICES AND INFORMATION
+Do Not Translate or Localize
+
+This software incorporates material from third parties.
+-------------------------------------------------------------------
+
+The external.rs file contains portions of llvm-sys's build.rs:
+llvm-sys 110.0.3 - SPDX: MIT - MIT License
+https://gitlab.com/taricorp/llvm-sys.rs
+https://gitlab.com/taricorp/llvm-sys.rs/-/blob/61b067d9d20f4603458ba5bc3ae93873fdadcfd0/build.rs
+
+/*
+Copyright (c) 2015 Peter Marheine
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+The target.c file contains llvm-sys's target.c:
+llvm-sys 110.0.3 - SPDX: MIT - MIT License
+https://gitlab.com/taricorp/llvm-sys.rs
+https://gitlab.com/taricorp/llvm-sys.rs/-/blob/61b067d9d20f4603458ba5bc3ae93873fdadcfd0/wrappers/target.c
+
+Copyright (c) 2015 Peter Marheine
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/qirlib/README.md b/qirlib/README.md
new file mode 100644
index 00000000..7815dfd5
--- /dev/null
+++ b/qirlib/README.md
@@ -0,0 +1,183 @@
+# qirlib
+
+`qirlib` is a set of APIs for generating, parsing, and evaluating [Quantum
+Intermediate Representation (QIR)](https://github.com/qir-alliance/qir-spec). It
+is also the native implementation behind the PyQIR packages:
+
+- [**pyqir-generator**](https://github.com/qir-alliance/pyqir/tree/main/pyqir-generator)
+- [**pyqir-evaluator**](https://github.com/qir-alliance/pyqir/tree/main/pyqir-evaluator)
+- [**pyqir-parser**](https://github.com/qir-alliance/pyqir/tree/main/pyqir-parser)
+
+## Requirements
+
+- Rust 1.57+
+- LLVM 11.x (see [Usage](#usage) below)
+
+## Usage
+
+### Summary
+
+`qirlib` requires a working LLVM installation to link against. By default, `qirlib`
+assumes a valid LLVM installation is available. This default is to align with the
+[`Inkwell`](https://github.com/thedan64/inkwell) and
+[`llvm-sys`](https://github.com/tari/llvm-sys.rs) defaults which are leveraged by
+`qirlib`. This default can be changed and `qirlib` provides features to
+bootstrap itself if desired. Building `qirlib` can be done with feature flags which will either:
+
+- (default) Use a local LLVM installation. This is either detected on the path or
+specified with an environment variable.
+- Build LLVM from source
+- Download a preexisting LLVM build
+
+Supported cargo features are following:
+
+| Feature                 | Link type               | Requirements        | Description                                            |
+|:------------------------|:------------------------|:--------------------|:-------------------------------------------------------|
+| `default`               | external | gcc/clang           | Includes `external-llvm-linking`     |
+| `external-llvm-linking` | external | gcc/clang           | Uses llvm-sys/inkwell for LLVM linking                 |
+| `no-llvm-linking`       |  none    |                     | Disable all LLVM linking. Used for local installation or packaging of LLVM. Includes `llvm-sys/disable-alltargets-init` and `inkwell/llvm11-0-no-llvm-linking` |
+| `qirlib-llvm-linking`  | internal  | `build-llvm` or `download-llvm` |  let qirlib do the LLVM linking. Includes `llvm-sys/disable-alltargets-init` and `inkwell/llvm11-0-no-llvm-linking` |
+| `download-llvm` | internal/none | cmake | dowload a precompiled version of LLVM|
+| `build-llvm`| internal/none | gcc/clang, cmake, ninja, git | Build LLVM from source. Installation defaults to `OUT_DIR/llvm` but can be overridden via the `QIRLIB_CACHE_DIR` environment variable.
+| `package-llvm` | none | cc/clang, cmake, ninja, git | *Dev use only* for packaging LLVM builds. Includes `build-llvm` and `no-llvm-linking`
+
+- Exactly one of the linking features is required:
+  - `qirlib-llvm-linking`
+  - `external-llvm-linking`
+  - `no-llvm-linking`
+- `build-llvm` and `download-llvm` cannot be used with `external-llvm-linking`
+
+### Environment variables
+
+Building `qirlib` can be done with crate features which will either download
+(and install) a preexisting LLVM build configured via environment variables
+(the
+[Cargo.toml [env] section](https://doc.rust-lang.org/nightly/cargo/reference/config.html#env)
+can also be used), build LLVM from source and install it, or build LLVM from
+source and package it
+
+- `QIRLIB_LLVM_EXTERNAL_DIR`
+  - Path to where LLVM is already installed by user. Useful if you want to use
+  your own LLVM builds for testing.
+- `QIRLIB_DOWNLOAD_LLVM`
+  - Indicator to whether the build should download LLVM cached builds.
+  - Build will download LLVM if needed unless this variable is defined and set to
+    `false`
+- `QIRLIB_LLVM_BUILDS_URL`
+  - Url from where LLVM builds will be downloaded.
+  - There is no default value provided. You can use the `package-llvm` feature
+    to create an archive and upload it along with SHA256 checksum file. The
+    checksum file must have the same file name with a file of the same name
+    with `.sha256` appended. The build will download both files and verify
+    that the checksum in the the file matches the downloaded archive.
+- `QIRLIB_CACHE_DIR`
+  - Root installation path for LLVM builds (mapped to `CMAKE_INSTALL_PREFIX`)
+  - Default if not specified when building from source:
+    - `qirlib`'s target `OUT_DIR`/llvm
+  - Default if not specified when installing:
+    - Linux/Mac: `$HOME/.pyqir`
+    - Windows: `$HOME\.pyqir`
+- `QIRLIB_LLVM_TAG`
+  - LLVM repo tag to fetch when building LLVM from source.
+  - Default value: `llvmorg-11.1.0`
+- `QIRLIB_LLVM_PKG_NAME`
+  - Optional name of package to be downloaded/created.
+- `LLVM_SYS_110_PREFIX`
+  - Required by `llvm-sys` and will be set to the version of LLVM used for
+  configuration.
+  - Version dependent and will change as LLVM is updated. (`LLVM_SYS_120_PREFIX`,
+  `LLVM_SYS_130_PREFIX`, etc)
+  - Not needed if you have a working LLVM installation on the path.
+
+### Using existing LLVM installation
+
+Using an existing (external) LLVM installation is the default and effects
+the linking that is used. The `default` feature enables the `external-llvm-linking` feature.
+
+```toml
+[features]
+# default to use llvm-sys/inkwell for llvm linking
+default = ["external-llvm-linking"]
+external-llvm-linking = []
+```
+
+[`llvm-sys`](https://github.com/tari/llvm-sys.rs) leveraged by `qirlib` and [`Inkwell`](https://github.com/thedan64/inkwell) will look for `llvm-config` on the path in order to determine how to link against LLVM. If this application is not found on the path, then the the `LLVM_SYS_<version>_PREFIX` environment variable is used to locate `llvm-config`. Only LLVM 11.x is supported at this time, so the exact environment variable name is `LLVM_SYS_110_PREFIX`.
+
+This environment variable can be set in your `Cargo.toml` in the `[env]` section or in the build environment.
+
+### Building (and linking) LLVM from source
+
+In order to build and link LLVM from source, we must also tell
+[`Inkwell`](https://github.com/thedan64/inkwell) and
+[`llvm-sys`](https://github.com/tari/llvm-sys.rs) to disable
+their own LLVM linking:
+
+```toml
+[features]
+# let qirlib do the llvm linking
+qirlib-llvm-linking = ["llvm-sys/disable-alltargets-init", "inkwell/llvm11-0-no-llvm-linking"]
+```
+
+Do to this, we must disable the default behavior (`external-llvm-linking`)  using:
+
+- The `--no-default-features` command-line flag disables the default features of the package.
+- The `default-features = false` option can be specified in a dependency declaration.
+
+```toml
+[dependencies]
+qirlib = { version = "0.3.0", default-features = false, features = "qirlib-llvm-linking,build-llvm" }
+```
+
+Or via the terminal (adding -vv so we can see build progress of LLVM)
+
+```bash
+qirlib> cargo build --release --no-default-features --features "qirlib-llvm-linking,build-llvm" -vv   
+```
+
+### Downloading (and linking) LLVM from pre-compiled binaries
+
+In order to build and link LLVM from source, we must also tell
+[`Inkwell`](https://github.com/thedan64/inkwell) and
+[`llvm-sys`](https://github.com/tari/llvm-sys.rs) to disable
+their own LLVM linking:
+
+```toml
+[features]
+# let qirlib do the llvm linking
+qirlib-llvm-linking = ["llvm-sys/disable-alltargets-init", "inkwell/llvm11-0-no-llvm-linking"]
+```
+
+Do to this, we must disable the default behavior (`external-llvm-linking`)  using:
+
+- The `--no-default-features` command-line flag disables the default features of the package.
+- The `default-features = false` option can be specified in a dependency declaration.
+
+```toml
+[dependencies]
+qirlib = { version = "0.3.0", default-features = false, features = "qirlib-llvm-linking,download-llvm" }
+```
+
+Or via the terminal (adding -vv so we can see download progress of LLVM)
+
+```bash
+qirlib> cargo build --release --no-default-features --features "qirlib-llvm-linking,download-llvm" -vv   
+```
+
+## Contributing
+
+There are many ways in which you can contribute to PyQIR, whether by
+contributing a feature or by engaging in discussions; we value contributions in
+all shapes and sizes! We refer to [this document](https://github.com/qir-alliance/pyqir/blob/main/CONTRIBUTING.md) for
+guidelines and ideas for how you can get involved.
+
+Contributing a pull request to this repo requires to agree to a [Contributor
+License Agreement
+(CLA)](https://en.wikipedia.org/wiki/Contributor_License_Agreement) declaring
+that you have the right to, and actually do, grant us the rights to use your
+contribution. A CLA-bot will automatically determine whether you need to provide
+a CLA and decorate the PR appropriately. Simply follow the
+instructions provided by the bot. You will only need to do this once.
+
+## Building and Testing
+
+See [Building](https://qir-alliance.github.io/pyqir/development-guide/building.html).
diff --git a/qirlib/build.rs b/qirlib/build.rs
new file mode 100644
index 00000000..023e1932
--- /dev/null
+++ b/qirlib/build.rs
@@ -0,0 +1,300 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::boxed::Box;
+use std::env;
+use std::error::Error;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use cc::Build;
+use cmake::Config;
+
+mod external;
+use external::llvm_sys;
+
+extern crate cc;
+#[macro_use]
+extern crate lazy_static;
+extern crate regex;
+extern crate semver;
+
+// Make sure exactly one of the linking features is used
+#[cfg(all(
+    not(any(feature = "qirlib-llvm-linking")),
+    not(any(feature = "external-llvm-linking")),
+    not(any(feature = "no-llvm-linking")),
+))]
+compile_error!("One of the features `qirlib/qirlib-llvm-linking`, `qirlib/external-llvm-linking`, and `qirlib/no-llvm-linking` must be used exclusive.");
+
+// Make sure only one linking option is used.
+#[cfg(any(
+    all(
+        feature = "qirlib-llvm-linking",
+        any(feature = "external-llvm-linking", feature = "no-llvm-linking")
+    ),
+    all(
+        feature = "external-llvm-linking",
+        any(feature = "qirlib-llvm-linking", feature = "no-llvm-linking")
+    ),
+    all(
+        feature = "no-llvm-linking",
+        any(feature = "qirlib-llvm-linking", feature = "external-llvm-linking")
+    ),
+))]
+compile_error!("Features `qirlib/qirlib-llvm-linking`, `qirlib/external-llvm-linking`, and `qirlib/no-llvm-linking` are mutually exclusive.");
+
+// if we are building or downloading, we cannot be externally linking
+#[cfg(any(
+    all(
+        feature = "build-llvm",
+        any(feature = "download-llvm", feature = "external-llvm-linking")
+    ),
+    all(
+        feature = "download-llvm",
+        any(feature = "build-llvm", feature = "external-llvm-linking")
+    ),
+))]
+compile_error!("Features `qirlib/build-llvm` and `qirlib/download-llvm` are mutually exclusive.");
+
+fn main() -> Result<(), Box<dyn Error>> {
+    println!("cargo:rerun-if-changed=build.rs");
+    println!("cargo:rerun-if-changed=config.cmake");
+    println!("cargo:rerun-if-changed=CMakeLists.txt");
+
+    let install_dir = get_llvm_install_dir();
+    println!("cargo:rerun-if-changed={:?}", install_dir);
+
+    // llvm-sys components
+    println!("cargo:rerun-if-changed=external.rs");
+    println!("cargo:rerun-if-changed=target.c");
+
+    // Download vars passed to cmake
+    println!("cargo:rerun-if-env-changed=QIRLIB_DOWNLOAD_LLVM");
+    println!("cargo:rerun-if-env-changed=QIRLIB_LLVM_BUILDS_URL");
+    println!("cargo:rerun-if-env-changed=QIRLIB_LLVM_PKG_NAME");
+
+    // Package vars used only in here
+    println!("cargo:rerun-if-env-changed=QIRLIB_PKG_DEST");
+
+    // Build vars passed to cmake
+    println!("cargo:rerun-if-env-changed=QIRLIB_LLVM_TAG");
+
+    // maps to CPACK_PACKAGE_FILE_NAME
+    println!("cargo:rerun-if-env-changed=QIRLIB_PACKAGE_FILE_NAME");
+
+    // maps to CMAKE_INSTALL_PREFIX passed to cmake in build and download
+    println!("cargo:rerun-if-env-changed=QIRLIB_CACHE_DIR");
+
+    if cfg!(feature = "download-llvm") {
+        println!("Downloading llvm");
+        download_llvm()?;
+    } else if cfg!(feature = "build-llvm") {
+        println!("Building llvm");
+        compile_llvm()?;
+    }
+    if cfg!(feature = "qirlib-llvm-linking") {
+        println!("Linking llvm");
+        link_llvm();
+        let build_dir = get_build_dir()?;
+        compile_target_wrappers(&build_dir);
+    } else if cfg!(feature = "external-llvm-linking") {
+        println!("LLVM_SYS_{{}}_PREFIX will provide the LLVM linking");
+    } else {
+        println!("No LLVM linking");
+    }
+
+    Ok(())
+}
+
+fn download_llvm() -> Result<(), Box<dyn Error>> {
+    // If the download url isn't set, we need to immediately fail.
+    let url = env::var("QIRLIB_LLVM_BUILDS_URL")?;
+
+    let enable_download = env::var("QIRLIB_DOWNLOAD_LLVM").unwrap_or_else(|_| "true".to_owned());
+
+    let build_dir = get_build_dir()?;
+
+    let mut config = Config::new(&build_dir);
+    config
+        .generator("Ninja")
+        .no_build_target(true)
+        .env("QIRLIB_LLVM_PKG_NAME", get_package_file_name()?)
+        .env("QIRLIB_LLVM_BUILDS_URL", url)
+        .env("QIRLIB_DOWNLOAD_LLVM", enable_download)
+        .define("CPACK_PACKAGE_FILE_NAME", get_package_name()?)
+        .define("CMAKE_INSTALL_PREFIX", get_llvm_install_dir())
+        .very_verbose(true);
+    let _ = config.build();
+
+    Ok(())
+}
+
+fn get_llvm_compile_target() -> String {
+    // We always install unless package is chosen.
+    // The user's choices for CMAKE_INSTALL_PREFIX will choose whether
+    // the installation goes into the target folder for linking or
+    // into another dir for potential reuse
+    if cfg!(feature = "package-llvm") {
+        "llvm-prefix/src/llvm-stamp/llvm-package".to_owned()
+    } else {
+        "llvm-prefix/src/llvm-stamp/llvm-install".to_owned()
+    }
+}
+
+fn compile_llvm() -> Result<(), Box<dyn Error>> {
+    let build_dir = get_build_dir()?;
+    let mut config = Config::new(&build_dir);
+
+    config
+        .generator("Ninja")
+        .build_target(get_llvm_compile_target().as_str())
+        .env("QIRLIB_LLVM_TAG", get_llvm_tag())
+        .define("CPACK_PACKAGE_FILE_NAME", get_package_name()?)
+        .define("CMAKE_INSTALL_PREFIX", get_llvm_install_dir());
+    let _ = config.build();
+
+    if cfg!(feature = "package-llvm") {
+        package_llvm()?;
+    }
+    Ok(())
+}
+
+fn package_llvm() -> Result<(), Box<dyn Error>> {
+    let out_dir = env::var("OUT_DIR").expect("Could not get OUT_DIR environment variable");
+    let output = PathBuf::from(out_dir)
+        .join("build")
+        .join("llvm-prefix")
+        .join("src")
+        .join("llvm-build")
+        .join(get_package_file_name()?);
+
+    if let Ok(dest_dir) = env::var("QIRLIB_PKG_DEST") {
+        let dest = PathBuf::from(dest_dir).join(get_package_file_name()?);
+        println!(
+            "Moving {} to {}.",
+            output.as_path().display(),
+            dest.as_path().display()
+        );
+        fs::rename(output, dest)?;
+    } else {
+        println!("Not moving package output. QIRLIB_PKG_DEST not set.");
+    }
+
+    Ok(())
+}
+
+fn get_build_dir() -> Result<PathBuf, Box<dyn Error>> {
+    let manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
+    let build_dir = PathBuf::from(manifest_dir.as_str());
+    let normalized_build_dir = fs::canonicalize(&build_dir)?;
+    println!(
+        "llvm build files dir: {}",
+        normalized_build_dir.to_str().unwrap()
+    );
+    Ok(normalized_build_dir)
+}
+
+fn link_llvm() {
+    let libdir = llvm_sys::llvm_config("--libdir");
+
+    // Export information to other crates
+    println!(
+        "cargo:config_path={}",
+        llvm_sys::LLVM_CONFIG_PATH.clone().unwrap().display()
+    ); // will be DEP_QIRLIB_CONFIG_PATH
+    println!("cargo:libdir={}", libdir); // DEP_QIRLIB_LIBDIR
+
+    // Link LLVM libraries
+    println!("cargo:rustc-link-search=native={}", libdir);
+    for name in llvm_sys::get_link_libraries() {
+        println!("cargo:rustc-link-lib=static={}", name);
+    }
+
+    // Link system libraries
+    for name in llvm_sys::get_system_libraries() {
+        println!("cargo:rustc-link-lib=dylib={}", name);
+    }
+}
+
+fn compile_target_wrappers(build_dir: &Path) {
+    let target_c = build_dir.join("target.c");
+    env::set_var("CFLAGS", llvm_sys::get_llvm_cflags());
+    Build::new().file(target_c).compile("targetwrappers");
+}
+
+fn get_package_file_name() -> Result<String, Box<dyn Error>> {
+    let mut base_name = get_package_name()?;
+
+    if llvm_sys::target_os_is("windows") {
+        base_name.push_str(".zip");
+    } else {
+        base_name.push_str(".tar.gz");
+    }
+
+    Ok(base_name)
+}
+
+fn get_llvm_tag() -> String {
+    if let Ok(tag) = env::var("QIRLIB_LLVM_TAG") {
+        tag
+    } else {
+        "llvmorg-11.1.0".to_owned() // 1fdec59bf
+    }
+}
+
+fn get_package_name() -> Result<String, Box<dyn Error>> {
+    if let Ok(file_name) = env::var("QIRLIB_PACKAGE_FILE_NAME") {
+        Ok(file_name)
+    } else {
+        let tag = get_llvm_tag();
+        let triple = get_target_triple()?;
+        let package_name = format!("qirlib-llvm-{}-{}", triple, tag);
+        Ok(package_name)
+    }
+}
+
+fn get_target_triple() -> Result<String, Box<dyn Error>> {
+    let target = if llvm_sys::target_os_is("windows") {
+        // TODO: remove static linking and just return the TARGET
+        "x86_64-pc-windows-msvc-static".to_owned()
+    } else {
+        env::var("TARGET")?
+    };
+    Ok(target)
+}
+
+fn get_llvm_install_dir() -> PathBuf {
+    if let Ok(path) = env::var("QIRLIB_CACHE_DIR") {
+        PathBuf::from(path)
+    } else {
+        // if we install to OUT_DIR the llvm install task during the extraction
+        // of the archive will empty the target directory breaking the build.
+        // To avoid that, we put llvm binaries into the OUT_DIR/llvm folder.
+        let out_dir = env::var("OUT_DIR").expect("Could not get OUT_DIR environment variable");
+        PathBuf::from(out_dir).join("llvm")
+    }
+}
+
+fn locate_llvm_config() -> Option<PathBuf> {
+    let dir = get_llvm_install_dir();
+    let prefix = dir.join("bin");
+    let binary_name = llvm_config_name();
+    let binary_path = prefix.join(binary_name);
+    if binary_path.as_path().exists() {
+        Some(binary_path)
+    } else {
+        None
+    }
+}
+
+pub fn llvm_config_name() -> String {
+    let mut base_name = "llvm-config".to_owned();
+
+    // On Windows, also search for llvm-config.exe
+    if llvm_sys::target_os_is("windows") {
+        base_name.push_str(".exe");
+    }
+
+    base_name
+}
diff --git a/qirlib/config.cmake b/qirlib/config.cmake
new file mode 100644
index 00000000..fe7c146b
--- /dev/null
+++ b/qirlib/config.cmake
@@ -0,0 +1,81 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+message(STATUS CMAKE_HOST_SYSTEM_NAME=${CMAKE_HOST_SYSTEM_NAME})
+
+if (${CMAKE_HOST_SYSTEM_NAME} MATCHES "Windows")
+  find_program(SCCACHE sccache)
+  if(SCCACHE)
+      set(LLVM_CCACHE_BUILD OFF CACHE BOOL "")
+      set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${SCCACHE}")
+      get_property(rule_launch_property GLOBAL PROPERTY RULE_LAUNCH_COMPILE)
+      message(STATUS RULE_LAUNCH_COMPILE=${rule_launch_property})
+      set(CMAKE_C_COMPILER_LAUNCHER "${SCCACHE}" CACHE STRING "")
+      message(STATUS CMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER})
+      set(CMAKE_CXX_COMPILER_LAUNCHER "${SCCACHE}" CACHE STRING "")
+      message(STATUS CMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER})
+  else()
+    message(STATUS "Not using sccache")
+  endif()
+else()
+  # Prepare cache if we find it
+  find_program(CCACHE ccache)
+  if(CCACHE)
+    message(STATUS "Using CCache for linux/darwin")
+    set(LLVM_CCACHE_BUILD ON CACHE BOOL "")
+    set(LLVM_CCACHE_DIR $ENV{CCACHE_DIR} CACHE STRING "")
+    set(LLVM_CCACHE_MAXSIZE "2G" CACHE STRING "")
+    message(STATUS CCACHE_DIR=$ENV{CCACHE_DIR})
+    message(STATUS LLVM_CCACHE_BUILD=${LLVM_CCACHE_BUILD})
+    message(STATUS LLVM_CCACHE_DIR=${LLVM_CCACHE_DIR})
+    message(STATUS LLVM_CCACHE_MAXSIZE=${LLVM_CCACHE_MAXSIZE})
+  else()
+    message(STATUS "Not using CCache")
+  endif()
+endif()
+
+# Set up main build props
+
+set(CMAKE_BUILD_TYPE MinSizeRel CACHE STRING "")
+
+set(LLVM_TARGETS_TO_BUILD "Native;X86" CACHE STRING "")
+
+set(PACKAGE_VENDOR LLVM.org CACHE STRING "")
+
+# Turn off
+set(LLVM_ENABLE_ASSERTIONS OFF CACHE BOOL "")
+set(LLVM_BUILD_EXAMPLES OFF CACHE BOOL "")
+set(LLVM_ENABLE_RTTI OFF CACHE BOOL "")
+
+set(LLVM_ENABLE_LIBXML2 OFF CACHE BOOL "")
+set(LLVM_ENABLE_LIBEDIT OFF CACHE BOOL "")
+set(LLVM_ENABLE_LIBPFM OFF CACHE BOOL "")
+set(LLVM_ENABLE_BINDINGS OFF CACHE BOOL "")
+set(LLVM_ENABLE_OCAMLDOC OFF CACHE BOOL "")
+set(LLVM_ENABLE_ZLIB OFF CACHE BOOL "")
+
+# Packing
+set(CPACK_BINARY_DEB OFF CACHE BOOL "")
+set(CPACK_BINARY_FREEBSD OFF CACHE BOOL "")
+set(CPACK_BINARY_IFW OFF CACHE BOOL "")
+set(CPACK_BINARY_NSIS OFF CACHE BOOL "")
+set(CPACK_BINARY_RPM OFF CACHE BOOL "")
+set(CPACK_BINARY_STGZ OFF CACHE BOOL "")
+set(CPACK_BINARY_TBZ2 OFF CACHE BOOL "")
+set(CPACK_BINARY_TXZ OFF CACHE BOOL "")
+set(CPACK_BINARY_TZ OFF CACHE BOOL "")
+
+if (${CMAKE_HOST_SYSTEM_NAME} MATCHES "Windows")
+  message(STATUS "Configuring for Windows")
+  set(LLVM_USE_CRT_RELEASE "MT" CACHE STRING "")
+  set(LLVM_USE_CRT_MINSIZEREL "MT" CACHE STRING "")
+  set(LLVM_BUILD_LLVM_C_DYLIB ON CACHE BOOL "")
+  set(CMAKE_INSTALL_UCRT_LIBRARIES ON CACHE BOOL "")
+  set(CPACK_BINARY_ZIP ON CACHE BOOL "")
+else()
+  set(LLVM_BUILD_LLVM_DYLIB ON CACHE BOOL "")
+  set(CPACK_BINARY_TGZ ON CACHE BOOL "")
+endif()
+
+
+
diff --git a/qirlib/external.rs b/qirlib/external.rs
new file mode 100644
index 00000000..e2afe5e9
--- /dev/null
+++ b/qirlib/external.rs
@@ -0,0 +1,186 @@
+/*
+Copyright (c) 2015 Peter Marheine
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+// In order to make sure we perform LLVM linking the same way as llvm-sys,
+// the following code is from its build.rs:
+// https://github.com/tari/llvm-sys.rs/blob/master/build.rs
+// Slight changes have been made to satisfy clippy and remove features
+// that we don't currently support.
+pub mod llvm_sys {
+    use std::{
+        env,
+        ffi::OsStr,
+        io,
+        path::{Path, PathBuf},
+        process::Command,
+    };
+
+    lazy_static! {
+        /// Filesystem path to an llvm-config binary for the correct version.
+        pub static ref LLVM_CONFIG_PATH: Option<PathBuf> = crate::locate_llvm_config();
+    }
+
+    /// Get the output from running `llvm-config` with the given argument.
+    ///
+    /// Lazily searches for or compiles LLVM as configured by the environment
+    /// variables.
+    pub fn llvm_config(arg: &str) -> String {
+        llvm_config_ex(&*LLVM_CONFIG_PATH.clone().unwrap(), arg)
+            .expect("Surprising failure from llvm-config")
+    }
+
+    /// Invoke the specified binary as llvm-config.
+    ///
+    /// Explicit version of the `llvm_config` function that bubbles errors
+    /// up.
+    pub fn llvm_config_ex<S: AsRef<OsStr>>(binary: S, arg: &str) -> io::Result<String> {
+        Command::new(binary)
+            .arg(arg)
+            .arg("--link-static") // Don't use dylib for >= 3.9
+            .output()
+            .map(|output| {
+                String::from_utf8(output.stdout)
+                    .expect("Output from llvm-config was not valid UTF-8")
+            })
+    }
+
+    pub fn get_llvm_cflags() -> String {
+        let output = llvm_config("--cflags");
+        if target_env_is("msvc") {
+            // MSVC doesn't accept -W... options, so don't try to strip them and
+            // possibly strip something that should be retained. Also do nothing if
+            // the user requests it.
+            return output;
+        }
+
+        llvm_config("--cflags")
+            .split(&[' ', '\n'][..])
+            .filter(|word| !word.starts_with("-W"))
+            .collect::<Vec<_>>()
+            .join(" ")
+    }
+
+    pub fn target_env_is(name: &str) -> bool {
+        match env::var_os("CARGO_CFG_TARGET_ENV") {
+            Some(s) => s == name,
+            None => false,
+        }
+    }
+
+    pub fn target_os_is(name: &str) -> bool {
+        match env::var_os("CARGO_CFG_TARGET_OS") {
+            Some(s) => s == name,
+            None => false,
+        }
+    }
+
+    /// Get the names of the dylibs required by LLVM, including the C++ standard
+    /// library.
+    pub fn get_system_libraries() -> Vec<String> {
+        llvm_config("--system-libs")
+            .split(&[' ', '\n'] as &[char])
+            .filter(|s| !s.is_empty())
+            .map(|flag| {
+                if cfg!(target_env = "msvc") {
+                    // Same as --libnames, foo.lib
+                    assert!(flag.ends_with(".lib"));
+                    &flag[..flag.len() - 4]
+                } else if cfg!(target_os = "macos") {
+                    // Linker flags style, -lfoo
+                    assert!(flag.starts_with("-l"));
+                    if flag.ends_with(".tbd") && flag.starts_with("-llib") {
+                        &flag[5..flag.len() - 4]
+                    } else {
+                        &flag[2..]
+                    }
+                } else {
+                    if flag.starts_with("-l") {
+                        // Linker flags style, -lfoo
+                        return flag
+                            .strip_prefix("-l")
+                            .expect("could not strip -l prefix")
+                            .to_owned();
+                    }
+
+                    let maybe_lib = Path::new(&flag);
+                    if maybe_lib.is_file() {
+                        // Library on disk, likely an absolute path to a .so
+                        if let Some(p) = maybe_lib.parent() {
+                            println!("cargo:rustc-link-search={}", p.display())
+                        }
+                        &maybe_lib.file_stem().unwrap().to_str().unwrap()[3..]
+                    } else {
+                        panic!("Unable to parse result of llvm-config --system-libs")
+                    }
+                }
+                .to_owned()
+            })
+            .chain(get_system_libcpp().map(str::to_owned))
+            .collect::<Vec<String>>()
+    }
+
+    /// Get the library that must be linked for C++, if any.
+    pub fn get_system_libcpp() -> Option<&'static str> {
+        if cfg!(target_env = "msvc") {
+            // MSVC doesn't need an explicit one.
+            None
+        } else if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
+            // On OS X 10.9 and later, LLVM's libc++ is the default. On earlier
+            // releases GCC's libstdc++ is default. Unfortunately we can't
+            // reasonably detect which one we need (on older ones libc++ is
+            // available and can be selected with -stdlib=lib++), so assume the
+            // latest, at the cost of breaking the build on older OS releases
+            // when LLVM was built against libstdc++.
+            Some("c++")
+        } else {
+            // Otherwise assume GCC's libstdc++.
+            // This assumption is probably wrong on some platforms, but would need
+            // testing on them.
+            Some("stdc++")
+        }
+    }
+
+    /// Get the names of libraries to link against.
+    pub fn get_link_libraries() -> Vec<String> {
+        // Using --libnames in conjunction with --libdir is particularly important
+        // for MSVC when LLVM is in a path with spaces, but it is generally less of
+        // a hack than parsing linker flags output from --libs and --ldflags.
+        llvm_config("--libnames")
+            .split(&[' ', '\n'] as &[char])
+            .filter(|s| !s.is_empty())
+            .map(|name| {
+                // --libnames gives library filenames. Extract only the name that
+                // we need to pass to the linker.
+                if cfg!(target_env = "msvc") {
+                    // LLVMfoo.lib
+                    assert!(name.ends_with(".lib"));
+                    &name[..name.len() - 4]
+                } else {
+                    // libLLVMfoo.a
+                    assert!(name.starts_with("lib") && name.ends_with(".a"));
+                    &name[3..name.len() - 2]
+                }
+            })
+            .map(str::to_owned)
+            .collect::<Vec<String>>()
+    }
+}
diff --git a/qirlib/src/lib.rs b/qirlib/src/lib.rs
index 8ab17e02..6a46f283 100644
--- a/qirlib/src/lib.rs
+++ b/qirlib/src/lib.rs
@@ -3,8 +3,17 @@
 
 #![warn(clippy::all, clippy::pedantic)]
 
+#[cfg(not(any(feature = "no-llvm-linking")))]
 pub mod codegen;
+
+#[cfg(not(any(feature = "no-llvm-linking")))]
 pub mod evaluation;
+
+#[cfg(not(any(feature = "no-llvm-linking")))]
 pub mod generation;
+
+#[cfg(not(any(feature = "no-llvm-linking")))]
 pub mod module;
+
+#[cfg(not(any(feature = "no-llvm-linking")))]
 pub mod passes;
diff --git a/qirlib/target.c b/qirlib/target.c
new file mode 100644
index 00000000..141a9fff
--- /dev/null
+++ b/qirlib/target.c
@@ -0,0 +1,70 @@
+/*
+Copyright (c) 2015 Peter Marheine
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/* llvm-c/Target.h helper functions wrappers.
+ *
+ * The LLVMInitializeAll* functions and friends are defined `static inline`, so
+ * we can't bind directly to them (the function body is generated via macro),
+ * so here are some wrappers.
+ */
+#include <llvm-c/Target.h>
+
+void LLVM_InitializeAllTargetInfos(void) {
+    LLVMInitializeAllTargetInfos();
+}
+
+void LLVM_InitializeAllTargets(void) {
+    LLVMInitializeAllTargets();
+}
+
+void LLVM_InitializeAllTargetMCs(void) {
+    LLVMInitializeAllTargetMCs();
+}
+
+void LLVM_InitializeAllAsmPrinters(void) {
+    LLVMInitializeAllAsmPrinters();
+}
+
+void LLVM_InitializeAllAsmParsers(void) {
+    LLVMInitializeAllAsmParsers();
+}
+
+void LLVM_InitializeAllDisassemblers(void) {
+    LLVMInitializeAllDisassemblers();
+}
+
+/* These functions return true on failure. */
+LLVMBool LLVM_InitializeNativeTarget(void) {
+    return LLVMInitializeNativeTarget();
+}
+
+LLVMBool LLVM_InitializeNativeAsmParser(void) {
+    return LLVMInitializeNativeAsmParser();
+}
+
+LLVMBool LLVM_InitializeNativeAsmPrinter(void) {
+    return LLVMInitializeNativeAsmPrinter();
+}
+
+LLVMBool LLVM_InitializeNativeDisassembler(void) {
+    return LLVMInitializeNativeDisassembler();
+}
\ No newline at end of file


Test patch:
diff --git a/pyqir-tests/tox.ini b/pyqir-tests/tox.ini
index 2af40c08..c3db4749 100644
--- a/pyqir-tests/tox.ini
+++ b/pyqir-tests/tox.ini
@@ -2,6 +2,11 @@
 isolated_build = true
 
 [testenv]
+# https://github.com/tox-dev/tox/issues/1550
+# PYTHONIOENCODING = utf-8 needs to be set to work around windows bug
+setenv =
+    PYTHONIOENCODING = utf-8
+
 deps =
     --editable=../pyqir-generator
     --editable=../pyqir-evaluator