diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index f669da25eb517af823faf33a9785c315384eeda6..243ac99fbf92d61703011c1a95dc88f1d5e23def 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -80,11 +80,8 @@ jobs:
         uses: actions/setup-java@v1
         with:
           java-version: 8
-      - name: set REPO_ROOT
-        run: echo "REPO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV
       - name: integration-tests
         run: ./gradlew :codegen-test:test
-      # TODO: when the multi repo integration tests are merged, update this to glob all of them
       - uses: actions/upload-artifact@v2
         name: Upload Codegen Output for inspection
         # Always upload the output even if the tests failed
@@ -92,7 +89,8 @@ jobs:
         with:
           name: codegen-output
           path: |
-            codegen-test/build/smithyprojections/codegen-test/source/rust-codegen/
+            codegen-test/build/smithyprojections/codegen-test/*/rust-codegen/
+            codegen-test/build/smithyprojections/codegen-test/Cargo.toml
             !**/target
   runtime-tests:
     name: Rust runtime tests
diff --git a/codegen-test/.gitignore b/codegen-test/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c063d7f5f71198f98c49ca8f88973d846bbc660e
--- /dev/null
+++ b/codegen-test/.gitignore
@@ -0,0 +1 @@
+smithy-build.json
diff --git a/codegen-test/README.md b/codegen-test/README.md
index 9e09e7b688b10cc531a8d3126895de1e8c22ec1f..a91b9adebd4d2e96a35c914030f50504f391c159 100644
--- a/codegen-test/README.md
+++ b/codegen-test/README.md
@@ -1,11 +1,8 @@
 # Codegen Integration Test
-This module defines an integration test of the code generation machinery. Models defined in `model` are built and generated into a Rust package. A `cargoCheck` Gradle task ensures that the generated Rust code compiles. This is added as a finalizer of the `test` task. 
+This module defines an integration test of the code generation machinery. `.build.gradle.kts` will generate a `smithy-build.json` file as part of the build. The Smithy build plugin then invokes our codegen machinery and generates Rust crates.
 
+The `test` task will run `cargo check` and `cargo clippy` to validate that the generated Rust compiles and is idiomatic.
 ## Usage
 ```
-# Compile codegen, Regenerate Rust, compile:
-# REPO_ROOT allows the runtime deps to be specified properly:
-REPO_ROOT=../ ../gradlew test
+../gradlew test
 ```
-
-The `smithy-build.json` configures the runtime dependencies to point directly to `../rust-runtime/*` via relative paths.
\ No newline at end of file
diff --git a/codegen-test/build.gradle.kts b/codegen-test/build.gradle.kts
index adcb07081d0ddfe83858d01d30ae9b255da1e929..8e6a6a69ee7d3373a3fa4e405ada53dd74b0a658 100644
--- a/codegen-test/build.gradle.kts
+++ b/codegen-test/build.gradle.kts
@@ -16,12 +16,77 @@ val smithyVersion: String by project
 
 dependencies {
     implementation(project(":codegen"))
+    implementation("software.amazon.smithy:smithy-aws-protocol-tests:$smithyVersion")
     implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion")
     implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion")
 }
 
+data class CodegenTest(val service: String, val module: String)
+
+val CodgenTests = listOf(
+    CodegenTest("com.amazonaws.dynamodb#DynamoDB_20120810", "dynamo"),
+    CodegenTest("com.amazonaws.ebs#Ebs", "ebs"),
+    CodegenTest("aws.protocoltests.json10#JsonRpc10", "json_rpc10")
+)
+
+fun generateSmithyBuild(tests: List<CodegenTest>): String {
+    val projections = tests.joinToString(",\n") {
+        """
+            "${it.module}": {
+                "plugins": { 
+                    "rust-codegen": { 
+                      "runtimeConfig": {
+                        "relativePath": "${rootProject.projectDir.absolutePath}/rust-runtime"
+                      },
+                      "service": "${it.service}",
+                      "module": "${it.module}",
+                      "moduleVersion": "0.0.1",
+                      "build": {
+                        "rootProject": true
+                      }
+                 } 
+               }
+            }
+        """.trimIndent()
+    }
+    return """
+    {
+        "version": "1.0",
+        "projections": { $projections }
+    }
+    """
+}
+
+
+task("generateSmithyBuild") {
+    description = "generate smithy-build.json"
+    doFirst {
+        projectDir.resolve("smithy-build.json").writeText(generateSmithyBuild(CodgenTests))
+    }
+}
+
+
+fun generateCargoWorkspace(tests: List<CodegenTest>): String {
+    return """
+    [workspace]
+    members = [
+        ${tests.joinToString(",") { "\"${it.module}/rust-codegen\"" }}
+    ]
+    """.trimIndent()
+}
+task("generateCargoWorkspace") {
+    description = "generate Cargo.toml workspace file"
+    doFirst {
+        buildDir.resolve("smithyprojections/codegen-test/Cargo.toml").writeText(generateCargoWorkspace(CodgenTests))
+    }
+}
+
+tasks["smithyBuildJar"].dependsOn("generateSmithyBuild")
+tasks["build"].finalizedBy("generateCargoWorkspace")
+
+
 tasks.register<Exec>("cargoCheck") {
-    workingDir("build/smithyprojections/codegen-test/source/rust-codegen/")
+    workingDir("build/smithyprojections/codegen-test/")
     // disallow warnings
     environment("RUSTFLAGS", "-D warnings")
     commandLine("cargo", "check")
@@ -29,7 +94,7 @@ tasks.register<Exec>("cargoCheck") {
 }
 
 tasks.register<Exec>("cargoClippy") {
-    workingDir("build/smithyprojections/codegen-test/source/rust-codegen/")
+    workingDir("build/smithyprojections/codegen-test/")
     // disallow warnings
     environment("RUSTFLAGS", "-D warnings")
     commandLine("cargo", "clippy")
@@ -37,3 +102,7 @@ tasks.register<Exec>("cargoClippy") {
 }
 
 tasks["test"].finalizedBy("cargoCheck", "cargoClippy")
+
+tasks["clean"].doFirst {
+    delete("smithy-build.json")
+}
diff --git a/codegen-test/smithy-build.json b/codegen-test/smithy-build.json
deleted file mode 100644
index d8f11ec326c0ea128cc2fa1e33a3c0143d31a35a..0000000000000000000000000000000000000000
--- a/codegen-test/smithy-build.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "version": "1.0",
-  "outputDirectory": "target",
-  "plugins": {
-    "rust-codegen": {
-      "runtimeConfig": {
-        "relativePath": "${REPO_ROOT}/rust-runtime"
-      },
-      "service": "com.amazonaws.ebs#Ebs",
-      "module": "weather",
-      "moduleVersion": "0.0.1",
-      "build": {
-        "rootProject": true
-      }
-    }
-  }
-}
diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt
index 26b2792b491629890c32ad1fc45e029ee0d5969c..ac993f025c064e89553a4a308a332ecd8738c576 100644
--- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt
+++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt
@@ -178,7 +178,8 @@ class SymbolVisitor(
     }
 
     override fun mapShape(shape: MapShape): Symbol {
-        require(shape.key.isStringShape)
+        val target = model.expectShape(shape.key.target)
+        require(target.isStringShape) { "unexpected key shape: ${shape.key}: $target [keys must be strings]" }
         val key = this.toSymbol(shape.key)
         val value = this.toSymbol(shape.value)
         return symbolBuilder(shape, RustType.HashMap(key.rustType(), value.rustType())).namespace(
diff --git a/test.sh b/test.sh
index 838965a68d652287615a752bb3b5f5a7b1b501e9..3935e22d9ecb1537d9bbe92fce4fafb33d6ee725 100755
--- a/test.sh
+++ b/test.sh
@@ -5,10 +5,6 @@
 #
 
 set -e
-# Used by codegen-test/smithy-build.json
-declare REPO_ROOT
-REPO_ROOT="$(git rev-parse --show-toplevel)"
-export REPO_ROOT
 ./gradlew test
 ./gradlew ktlintFormat
 ./gradlew ktlint
\ No newline at end of file