Repository: GoogleContainerTools/skaffold
Pull request #6851 (2021-11-12 02:15:54)

fix: skaffold build fails when attempting to COPY an empty dir

Signed-off-by: Pablo Caderno <kaderno@gmail.com>

<!-- Thank you for your contribution! -->

<!-- Include if applicable: -->
Fixes: #6810 
Related: #6796 
**Merge before/after**: _Dependent or prerequisite PRs_

**Description**
<!-- Describe your changes here. The more detail, the easier the review! -->
Refactored function WalkWorkspace to also run over directories. If directory wasn't empty, by copying the file it would create also the directory, but if the directory is empty there is some code region that couldn't be reached the way it was written.


**Follow-up Work (remove if N/A)**
The code I've added has some duplication that I'm not a huge fan of that could be easily replaced if there's an easy/better way to figure out that a directory is empty.

If this is merged, the test introduced within #6796 can be deleted as this would replace it.



codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#6851](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (6d12de4) into [main](https://codecov.io/gh/GoogleContainerTools/skaffold/commit/290280e8c6c19231e9ed5b5c027f2c01d22f75be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (290280e) will **decrease** coverage by `1.36%`.
> The diff coverage is `64.65%`.

[![Impacted file tree graph](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/graphs/tree.svg?width=650&height=150&src=pr&token=mVPJs21wYu&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)

```diff
@@            Coverage Diff             @@
##             main    #6851      +/-   ##
==========================================
- Coverage   70.48%   69.11%   -1.37%     
==========================================
  Files         515      547      +32     
  Lines       23150    25067    +1917     
==========================================
+ Hits        16317    17326    +1009     
- Misses       5776     6573     +797     
- Partials     1057     1168     +111     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | Coverage Δ | |
|---|---|---|
| [cmd/skaffold/app/cmd/deploy.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `52.00% <ø> (-1.85%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/dev.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGV2Lmdv) | `84.61% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/flags.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZmxhZ3MuZ28=) | `91.00% <0.00%> (+0.18%)` | :arrow_up: |
| [cmd/skaffold/app/cmd/render.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcmVuZGVyLmdv) | `36.66% <0.00%> (-4.72%)` | :arrow_down: |
| [cmd/skaffold/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL3NrYWZmb2xkLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/inspect\_tests.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF90ZXN0cy5nbw==) | `62.50% <14.28%> (-1.14%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/lint.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbGludC5nbw==) | `42.85% <42.85%> (ø)` | |
| [cmd/skaffold/app/cmd/find\_configs.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZmluZF9jb25maWdzLmdv) | `48.88% <50.00%> (+0.24%)` | :arrow_up: |
| [cmd/skaffold/app/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9za2FmZm9sZC5nbw==) | `76.19% <70.00%> (-8.43%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/inspect\_build\_env.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF9idWlsZF9lbnYuZ28=) | `65.11% <75.00%> (+6.39%)` | :arrow_up: |
| ... and [158 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools). Last update [b32a1e8...6d12de4](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/6851?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools).

kadern0: @briandealwis, according to this comment on the `WalkWorkspace` function: `// Note: if you change this function, you might also want to modify `walkWorkspaceWithDestinations`.`  we might still have some work to do. 

On a quick look, this function seems to also ignore empty directories. I'm going to add a `WIP` to the PR and see what else I can find.
kadern0: Hi @briandealwis , I think I'm going to need some help here. I think this code change fixes the issue, but I cannot create an appropriate test for it. I was trying to modify the integration test but I'm struggling with the empty directory creation since you cannot add empty dirs on git (which I did not know until working on this). The error thrown on the integration tests means that the `testdir` is not being created where it was supposed to so I'm wondering how to do that properly. I'll be offline for 10 days so I won't be able to work on this until then. 
SSGoku369: ok
MarlonGamez: hi @kadern0, sorry for disappearing on this PR for so long. Are you still interested on working on this? We can try to help out, however, we're planning on a code freeze for a bit this upcoming week in order to prepare for a large merge we're doing. If you're still interested in working on this, we can leave it open and pick it up once that merge is done 
kadern0: Hi @MarlonGamez, happy to work on this after the merge, although I think I need a hand to create a proper test for this. 
aaron-prindle: Friendly ping @kadern0, sorry for the delay with the PR here.  Is there any change you can resubmit your changes such that we can get CI green and we can get this over the finish line?  Thanks!
kadern0: Thanks for the ping. I'm finishing my holidays, should be able to work on
it towards the end of next week.

On Tue, 26 July 2022, 20:33 Aaron Prindle, ***@***.***> wrote:

> Friendly ping @kadern0 <https://github.com/kadern0>, sorry for the delay
> with the PR here. Is there any change you can resubmit your changes such
> that we can get CI green and we can get this over the finish line? Thanks!
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/GoogleContainerTools/skaffold/pull/6851#issuecomment-1195841410>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ADEHQTBFKIGNYR6OU4QMN43VWAVOLANCNFSM5H3ZJACA>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

kadern0: @aaron-prindle, I've rebased the branch and quickly performed some tests locally. Whilst it seems to be doing what's expected, I've removed the WIP from the PR so all the tests are triggered although some further inspection would be appreciated.

Patch:
diff --git a/docs-v2/content/en/docs/_index.md b/docs-v2/content/en/docs/_index.md
index c83e27bf0..4249cc1cf 100644
--- a/docs-v2/content/en/docs/_index.md
+++ b/docs-v2/content/en/docs/_index.md
@@ -24,6 +24,7 @@ Kubernetes cluster.
 * Skaffold projects work everywhere
   * **share with other developers** - Skaffold is the easiest way to **share your project** with the world: `git clone` and `skaffold run`
   * **context aware** - use Skaffold profiles, local user config, environment variables, and flags to easily incorporate differences across environments
+  * **platform aware** - use cross-platform and multi-platform **build** support, with automatic platform detection, to easily handle operating system and architecture differences between the development machine and Kubernetes cluster nodes.
   * **CI/CD building blocks** - use `skaffold build`, `skaffold test` and `skaffold deploy` as part of your CI/CD pipeline, or simply `skaffold run` end-to-end
   * **GitOps integration** - use `skaffold render` to build your images and render templated Kubernetes manifests for use in GitOps workflows
 * skaffold.yaml - a single pluggable, declarative configuration for your project
diff --git a/docs-v2/content/en/docs/pipeline-stages/builders/_index.md b/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
index 76c7f6bf4..d8af95356 100644
--- a/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
+++ b/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
@@ -162,9 +162,9 @@ Skaffold currently supports [Docker]({{<relref "/docs/pipeline-stages/builders/d
 [Jib]({{<relref "/docs/pipeline-stages/builders/jib#remotely-with-google-cloud-build">}})
 on Google Cloud Build.
 
-## Cross-platform build support
+## Cross-platform and Multi-platform build support
 
-Skaffold selectively supports building for a platform that is different than the host machine platform. The target platform for an artifact can be specified in one of the following ways:
+Skaffold selectively supports building for an architecture that is different than the development machine architecture (`cross-platform` build) or building for multiple architectures (`multiple-platform` build). The target platforms for an artifact can be specified in one of the following ways:
 
 - The pipeline's `platforms` property in the `skaffold.yaml` file.
 {{% readfile file="samples/builders/platforms/pipeline-constraints.yaml" %}}
@@ -175,7 +175,7 @@ Skaffold selectively supports building for a platform that is different than the
 - The CLI flag `--platform` which overrides the values set in both the previous ways.
 
 ```cmd
-skaffold build --platform=linux/arm64
+skaffold build --platform=linux/arm64,linux/amd64
 ```
 
 Additionally, for `skaffold dev`, `skaffold debug` and `skaffold run` commands, where the build output gets deployed immediately, skaffold checks the platform for the kubernetes cluster nodes and attempts to build artifacts for that target platform.
@@ -184,7 +184,7 @@ The final list of target platforms need to ultimately be supported by the target
 
 |    | Local Build | In Cluster Build | Remote on Google Cloud Build |
 |----|:-----------:|:----------------:|:----------------------------:|
-| **Dockerfile** | Cross-platform supported | Cross-platform supported but platform should match cluster node running the pod. | Cross-platform supported |
+| **Dockerfile** | Cross-platform and multi-platform supported | Cross-platform supported but platform should match cluster node running the pod. | Cross-platform and multi-platform supported |
 | **Jib Maven and Gradle** | Cross-platform and multi-platform supported | - | Cross-platform and multi-platform supported |
 | **Cloud Native Buildpacks** | Only supports `linux/amd64` | - | Only supports `linux/amd64` |
 | **Bazel** | Cross-platform supported but requires explicit platform specific rules. Not yet implemented | - | - |
@@ -192,5 +192,5 @@ The final list of target platforms need to ultimately be supported by the target
 | **Custom Script** | Cross-platform and multi-platform supported but requires user to implement it in the build script | Cross-platform and multi-platform supported but requires user to implement it in the build script | - |
 
 {{< alert title="Note" >}}
-Skaffold supports multi-platform image builds using the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}).
+Skaffold supports multi-platform image builds natively for the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}). For other builders that support building cross-architecture images, Skaffold will iteratively build a single platform image for each target architecture and stitch them together into a multi-platform image, and push it to the registry.
 {{< /alert >}}
diff --git a/docs-v2/content/en/docs/workflows/handling-platforms.md b/docs-v2/content/en/docs/workflows/handling-platforms.md
new file mode 100644
index 000000000..072ca54f4
--- /dev/null
+++ b/docs-v2/content/en/docs/workflows/handling-platforms.md
@@ -0,0 +1,231 @@
+---
+title: "Managing ARM workloads [NEW]"
+linkTitle: "Managing ARM workloads [NEW]"
+featureId: build.platforms
+weight: 50
+---
+
+Skaffold has a lot of intelligence built-in to simplify working with ARM workloads. Whether developing on an Apple Silicon Macbook that uses an ARM based chip, or deploying to a GKE Kubernetes cluster having ARM nodes, Skaffold can take away the complexities that arise when the architecture of your development machine and Kubernetes cluster don't match.
+
+## Why is image architecture important?
+
+Container images are built targeting specific [Instruction Set Architectures](https://en.wikipedia.org/wiki/Instruction_set_architecture) like `amd64`, `arm64`, etc. **You must use container images that are compatible with the architecture of the node where you intend to run the workloads.** For example, to deploy to a GKE cluster running ARM nodes, the image needs to be built for `linux/arm64` platform.
+
+All image builders build for different default architecture and not all support cross-architecture builds. For instance [Docker]({{<relref "/docs/pipeline-stages/builders/docker">}}) will build the image for the same architecture as the host machine, whereas [Buildpacks]({{<relref "/docs/pipeline-stages/builders/buildpacks">}}) will always build it for `amd64`.
+
+Additionally, the following combination of development machine and cluster node architectures can make it difficult to build and deploy images correctly:
+
+* Dev machine architecture is `amd64` while the target cluster runs `arm64` nodes.
+* Dev machine architecture is `arm64` (say Apple Silicon Macbooks) while the target cluster runs `amd64` nodes.
+* The target cluster runs both `arm64` and `amd64` nodes (mixed node pools).
+
+🎉 *Skaffold provides an opionated way to handle all these cases effectively.* 🎉
+
+## Skaffold can set the image architecture automatically
+
+When running Skaffold in an interactive mode like `skaffold dev`, `skaffold debug` or `skaffold run` where the intention is to build an image from the application code, and immediately deploy it to a Kubernetes cluster, Skaffold will check the active Kubernetes cluster node architecture and provide that as an argument to the respective image builder. If the cluster has multiple architecture nodes, then Skaffold will also create appropriate Kubernetes [`affinity`](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) rules so that the Kubernetes Pods with these images are assigned to matching architecture nodes.
+
+{{< alert title="Note" >}}
+Skaffold will create platform node `affinity` rules only for clusters having multiple architecture nodes. You can also force this using the flag `--enable-platform-node-affinity=true` to always create these affinity rules in the Kubernetes manifests for built images.
+{{< /alert >}}
+
+Let's test this in a [sample Golang](https://github.com/GoogleContainerTools/skaffold/tree/main/examples/cross-platform-builds) project:
+
+* The `skaffold.yaml` file defines a single [Docker build artifact](https://github.com/GoogleContainerTools/skaffold/blob/main/examples/cross-platform-builds/Dockerfile) and deploys it in a [`Kubernetes Pod`](https://github.com/GoogleContainerTools/skaffold/blob/main/examples/cross-platform-builds/k8s-pod.yaml).
+
+* First set the active Kubernetes context to a cluster having only `linux/amd64` nodes, and run:
+
+  ```cmd
+  skaffold dev --default-repo=your/container/registy
+  ```
+
+* Skaffold will detect the cluster node platform `linux/amd64` and build the image for this platform:
+
+  ```cmd
+  skaffold dev --default-repo=gcr.io/k8s-skaffold
+  Listing files to watch...
+  - skaffold-example
+  Generating tags...
+  - skaffold-example -> gcr.io/k8s-skaffold/skaffold-example:latest
+  Starting build...
+  Building [skaffold-example]...
+  Target platforms: [linux/amd64]
+  ...
+  Build [skaffold-example] succeeded
+  Starting deploy...
+  - pod/getting-started created
+  Waiting for deployments to stabilize...
+  - pods is ready.
+  Deployments stabilized in 7.42 seconds
+  Press Ctrl+C to exit
+  Watching for changes...
+  [getting-started] Hello world! Running on linux/amd64
+  ```
+
+* Now set the active Kubernetes context to a cluster containing only `linux/arm64` nodes. See [here](https://cloud.google.com/kubernetes-engine/docs/how-to/prepare-arm-workloads-for-deployment) to know how you can create an ARM GKE cluster.
+
+* Re-running the `dev` command will now build a `linux/arm64` image.
+
+  ```cmd
+  skaffold dev --default-repo=gcr.io/k8s-skaffold
+  ...
+  ...
+  [getting-started] Hello world! Running on linux/arm64
+  ```
+
+* Now set the active Kubernetes context to a cluster containing both `linux/arm64` and `linux/amd64` nodes. You can create a GKE cluster with 2 node pools, one having `linux/amd64` nodes, and the other having `linux/arm64` nodes.
+
+* Re-run the `dev` command but with an explicit platform target this time.
+
+  ```cmd
+  skaffold dev --default-repo=your/container/registy --platform=linux/amd64
+  ```
+
+* Skaffold will build a `linux/amd64` image and insert a `nodeAffinity` definition to the `Pod` so that it get scheduled on the matching architecture node.
+
+  ```cmd
+  skaffold dev --default-repo=gcr.io/k8s-skaffold --platform=linux/amd64
+  ...
+  ...
+  [getting-started] Hello world! Running on linux/amd64
+  ```
+
+* Validate that the `nodeAffinity` was applied by running the command (skip `| jq` if you don't have `jq` installed):
+
+  ```cmd
+  kubectl get pod getting-started  -o=jsonpath='{.spec.affinity}' | jq
+  {
+    "nodeAffinity": {
+      "requiredDuringSchedulingIgnoredDuringExecution": {
+        "nodeSelectorTerms": [
+          {
+            "matchExpressions": [
+              {
+                "key": "kubernetes.io/os",
+                "operator": "In",
+                "values": [
+                  "linux"
+                ]
+              },
+              {
+                "key": "kubernetes.io/arch",
+                "operator": "In",
+                "values": [
+                  "amd64"
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }
+  ```
+
+This example will run the same whether you're using an `arm64` machine (say an Apple Silicon Macbook) or an `amd64` machine.
+
+Skaffold also supports cross-architecture builds on [Google Cloud Build](https://cloud.google.com/build). You can rerun this example, with the additional flag `--profile cloudbuild` to all the `dev` commands to build on `Google Cloud Build` instead of the local Docker daemon.
+
+## What about multi-arch images?
+
+A [multi-arch image](https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/) is an image that can support multiple architectures. It looks like a single image with a single tag, but is actually a list of images targeting multiple architectures organized by an [image index](https://github.com/opencontainers/image-spec/blob/main/image-index.md). When you deploy a multi-arch image to a cluster, the container runtime automatically chooses the right image that is compatible with the architecture of the node to which it is being deployed. This simplifies targeting multiple clusters of different architecture nodes, and/or mixed-architecture nodes.
+
+Skaffold supports building multi-platform images natively for the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}). For other builders that support building cross-architecture images, Skaffold will iteratively build a single platform image for each target architecture and stitch them together into a multi-platform image, and push it to the registry.
+
+Let's test this in the same [sample Golang](https://github.com/GoogleContainerTools/skaffold/tree/main/examples/cross-platform-builds) project as before:
+
+* Run this command to build for the target architectures `linux/amd64` and `linux/arm64`:
+
+  ```cmd
+  skaffold dev -t latest --default-repo=your/container/registy --platform=linux/amd64,linux/arm64
+  ...
+  Building [skaffold-example]...
+  Target platforms: [linux/amd64,linux/arm64]
+  ...
+  [+] Building 0.3s (13/13) FINISHED
+  ...
+  => => writing image sha256:10af3142e460566f5791c48758f0040cef6932cbcb0766082dcbb0d8db7653e7
+  => => naming to gcr.io/k8s-skaffold/skaffold-example:latest_linux_amd64
+  ...
+  latest_linux_amd64: digest: sha256:15bd4f2380e99b3563f8add1aba9691e414d4cc5701363d9c74960a20fb276c4 size: 739
+  ...
+  [+] Building 52.8s (13/13) FINISHED
+  ...
+  => => writing image sha256:68866691e2d6f079b116e097ae4e67a53eaf89e825b52d6f31f2e9cc566974de
+  => => naming to gcr.io/k8s-skaffold/skaffold-example:latest_linux_arm64
+  ...
+  4e0c2525c370: Pushed
+  latest_linux_arm64: digest: sha256:868d0aec1cc7d2ed1fa1e840f38ff1aa50c3cc3d3232ea17a065618eaec4e82b size: 739
+  Build [skaffold-example] succeeded
+  ```
+
+* Validate that the image just built was multi-arch, by running the following `docker` command:
+
+  ```cmd
+  docker manifest inspect your/container/registry/skaffold-example:latest | grep -A 3 "platform"
+  ```
+
+  Outputs:
+
+  ```cmd
+    "platform": {
+        "architecture": "amd64",
+        "os": "linux"
+    }
+  --
+    "platform": {
+        "architecture": "arm64",
+        "os": "linux"
+    }
+  ```
+
+* Now if we render the Kubernetes Pod manifest for this multi-arch image, then it'll have platform affinity definition targeting both `linux/amd64` and `linux/arm64` architectures.
+
+  ```cmd
+  skaffold render --default-repo=your/container/registry --enable-platform-node-affinity
+  ```
+  
+  Outputs:
+
+  ```cmd
+  apiVersion: v1
+  kind: Pod
+  metadata:
+    name: getting-started
+    namespace: default
+  spec:
+    affinity:
+      nodeAffinity:
+        requiredDuringSchedulingIgnoredDuringExecution:
+          nodeSelectorTerms:
+          - matchExpressions:
+            - key: kubernetes.io/os
+              operator: In
+              values:
+              - linux
+            - key: kubernetes.io/arch
+              operator: In
+              values:
+              - amd64
+          - matchExpressions:
+            - key: kubernetes.io/os
+              operator: In
+              values:
+              - linux
+            - key: kubernetes.io/arch
+              operator: In
+              values:
+              - arm64
+    containers:
+    - image: gcr.io/k8s-skaffold/skaffold-example:latest@sha256:9ecf4e52f7ff64b35deacf9d6eedc03f35d69e0b4bf3679b97ba492f4389f784
+      name: getting-started
+  ```
+
+{{< alert title="Note" >}}
+
+* Multi-arch images need to be pushed to a container registry, as the local Docker deamon doesn't yet support storing multi-arch images.
+
+* For interactive modes like `skaffold dev` and `skaffold debug` requiring fast and repeated `build-render-deploy` iterations, Skaffold will choose only one build architecture and build a single-platform image, even if you specify multiple target platforms.
+
+* If you need to build a multi-arch image with an interactive mode then use `skaffold run`. This will build the multi-arch image and deploy it to the active Kubernetes cluster.
+{{< /alert >}}
diff --git a/docs-v2/data/maturity.json b/docs-v2/data/maturity.json
index 09aa2a540..061a10c69 100644
--- a/docs-v2/data/maturity.json
+++ b/docs-v2/data/maturity.json
@@ -41,6 +41,18 @@
     "maturity": "GA",
     "description": "Define build artifact dependencies"
   },
+  "build.platforms": {
+    "dev": "x",
+    "build": "x",
+    "run": "x",
+    "debug": "x",
+    "render": "x",
+    "deploy": "x",
+    "area": "Build",
+    "feature": "Cross-arch and Multi-arch builds",
+    "maturity": "alpha",
+    "description": "Build cross-architecture and Multi-architecture images"
+  },
   "build.ko": {
     "dev": "x",
     "build": "x",
@@ -231,6 +243,15 @@
     "maturity": "alpha",
     "description": "User can generate a starter tekton pipeline using skaffold"
   },
+  "verify": {
+    "dev": "x",
+    "debug": "x",
+    "run": "x",
+    "deploy": "x",
+    "area": "skaffold verify",
+    "maturity": "beta",
+    "description": "User can run post deployment tests via skaffold monitored test containers"
+  },
   "global_config": {
     "dev": "x",
     "build": "x",
diff --git a/examples/buildpacks-python/requirements.txt b/examples/buildpacks-python/requirements.txt
index cf6ed5429..c9edc1a7f 100644
--- a/examples/buildpacks-python/requirements.txt
+++ b/examples/buildpacks-python/requirements.txt
@@ -1,1 +1,1 @@
-Flask==2.2.1
+Flask==2.2.2
diff --git a/examples/cross-platform-builds/Dockerfile b/examples/cross-platform-builds/Dockerfile
index 326833f37..67f8c3cbb 100644
--- a/examples/cross-platform-builds/Dockerfile
+++ b/examples/cross-platform-builds/Dockerfile
@@ -1,12 +1,10 @@
-FROM --platform=$BUILDPLATFORM golang:1.18 as builder
+FROM golang:1.18 as builder
 WORKDIR /code
 COPY main.go .
 COPY go.mod .
 # `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
-ARG TARGETOS
-ARG TARGETARCH
 ARG SKAFFOLD_GO_GCFLAGS
-RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
+RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
 
 FROM alpine:3
 # Define GOTRACEBACK to mark this container as using the Go language runtime
diff --git a/examples/cross-platform-builds/skaffold.yaml b/examples/cross-platform-builds/skaffold.yaml
index bfad2ba76..1dee227e8 100644
--- a/examples/cross-platform-builds/skaffold.yaml
+++ b/examples/cross-platform-builds/skaffold.yaml
@@ -1,7 +1,6 @@
 apiVersion: skaffold/v2beta29
 kind: Config
 build:
-  platforms: ["linux/amd64"]
   artifacts:
   - image: skaffold-example
 deploy:
@@ -9,7 +8,6 @@ deploy:
     manifests:
       - k8s-*
 profiles:
-  - name: googleCloudBuild
+  - name: cloudbuild
     build:
-      googleCloudBuild:
-        projectId: k8s-skaffold
+      googleCloudBuild: {}
diff --git a/examples/docker-deploy/bert/Dockerfile b/examples/docker-deploy/bert/Dockerfile
index c01cebfca..8bea1d135 100644
--- a/examples/docker-deploy/bert/Dockerfile
+++ b/examples/docker-deploy/bert/Dockerfile
@@ -1,5 +1,6 @@
 FROM golang:1.18 as builder
 WORKDIR /code
+COPY test .
 COPY main.go .
 COPY go.mod .
 # `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
diff --git a/integration/examples/buildpacks-python/requirements.txt b/integration/examples/buildpacks-python/requirements.txt
index cf6ed5429..c9edc1a7f 100644
--- a/integration/examples/buildpacks-python/requirements.txt
+++ b/integration/examples/buildpacks-python/requirements.txt
@@ -1,1 +1,1 @@
-Flask==2.2.1
+Flask==2.2.2
diff --git a/integration/examples/cross-platform-builds/Dockerfile b/integration/examples/cross-platform-builds/Dockerfile
index 326833f37..67f8c3cbb 100644
--- a/integration/examples/cross-platform-builds/Dockerfile
+++ b/integration/examples/cross-platform-builds/Dockerfile
@@ -1,12 +1,10 @@
-FROM --platform=$BUILDPLATFORM golang:1.18 as builder
+FROM golang:1.18 as builder
 WORKDIR /code
 COPY main.go .
 COPY go.mod .
 # `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
-ARG TARGETOS
-ARG TARGETARCH
 ARG SKAFFOLD_GO_GCFLAGS
-RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
+RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
 
 FROM alpine:3
 # Define GOTRACEBACK to mark this container as using the Go language runtime
diff --git a/integration/examples/cross-platform-builds/skaffold.yaml b/integration/examples/cross-platform-builds/skaffold.yaml
index 2b8de1eec..3a3303969 100644
--- a/integration/examples/cross-platform-builds/skaffold.yaml
+++ b/integration/examples/cross-platform-builds/skaffold.yaml
@@ -8,10 +8,12 @@ build:
       dockerfile: Dockerfile
   tagPolicy:
     gitCommit: {}
-  platforms:
-  - linux/amd64
   local:
     concurrency: 1
 manifests:
   rawYaml:
   - k8s-*
+profiles:
+  - name: cloudbuild
+    build:
+      googleCloudBuild: {}
\ No newline at end of file
diff --git a/integration/testdata/getting-started/testdir/testfile b/integration/testdata/getting-started/testdir/testfile
deleted file mode 100644
index e69de29bb..000000000
diff --git a/pkg/skaffold/build/builder_mux.go b/pkg/skaffold/build/builder_mux.go
index 788c201a1..d2bdfac2c 100644
--- a/pkg/skaffold/build/builder_mux.go
+++ b/pkg/skaffold/build/builder_mux.go
@@ -37,6 +37,11 @@ type BuilderMux struct {
 	byImageName map[string]PipelineBuilder
 	store       ArtifactStore
 	concurrency int
+	cache       Cache
+}
+
+type Cache interface {
+	AddArtifact(ctx context.Context, a graph.Artifact) error
 }
 
 // Config represents an interface for getting all config pipelines.
@@ -49,7 +54,7 @@ type Config interface {
 }
 
 // NewBuilderMux returns an implementation of `build.BuilderMux`.
-func NewBuilderMux(cfg Config, store ArtifactStore, builder func(p latest.Pipeline) (PipelineBuilder, error)) (*BuilderMux, error) {
+func NewBuilderMux(cfg Config, store ArtifactStore, cache Cache, builder func(p latest.Pipeline) (PipelineBuilder, error)) (*BuilderMux, error) {
 	pipelines := cfg.GetPipelines()
 	m := make(map[string]PipelineBuilder)
 	var pbs []PipelineBuilder
@@ -64,7 +69,7 @@ func NewBuilderMux(cfg Config, store ArtifactStore, builder func(p latest.Pipeli
 		}
 	}
 	concurrency := getConcurrency(pbs, cfg.BuildConcurrency())
-	return &BuilderMux{builders: pbs, byImageName: m, store: store, concurrency: concurrency}, nil
+	return &BuilderMux{builders: pbs, byImageName: m, store: store, concurrency: concurrency, cache: cache}, nil
 }
 
 // Build executes the specific image builder for each artifact in the given artifact slice.
@@ -101,6 +106,14 @@ func (b *BuilderMux) Build(ctx context.Context, out io.Writer, tags tag.ImageTag
 		if built, err = artifactBuilder(ctx, out, artifact, tag, platforms); err != nil {
 			return "", err
 		}
+
+		if err := b.cache.AddArtifact(ctx, graph.Artifact{
+			ImageName: artifact.ImageName,
+			Tag:       built,
+		}); err != nil {
+			log.Entry(ctx).Warnf("error adding artifact to cache; caching may not work as expected: %v", err)
+		}
+
 		if err = r.RunPostHooks(ctx, out); err != nil {
 			return "", err
 		}
diff --git a/pkg/skaffold/build/cache/cache.go b/pkg/skaffold/build/cache/cache.go
index d6755f88d..6dbda434f 100644
--- a/pkg/skaffold/build/cache/cache.go
+++ b/pkg/skaffold/build/cache/cache.go
@@ -48,6 +48,7 @@ type ArtifactCache map[string]ImageDetails
 // cache holds any data necessary for accessing the cache
 type cache struct {
 	artifactCache      ArtifactCache
+	hashByName         map[string]string
 	artifactGraph      graph.ArtifactGraph
 	artifactStore      build.ArtifactStore
 	cacheMutex         sync.RWMutex
@@ -91,6 +92,8 @@ func NewCache(ctx context.Context, cfg Config, isLocalImage func(imageName strin
 		return &noCache{}, nil
 	}
 
+	hashByName := make(map[string]string)
+
 	client, err := docker.NewAPIClient(ctx, cfg)
 	if err != nil {
 		// error only if any pipeline is local.
@@ -117,6 +120,7 @@ func NewCache(ctx context.Context, cfg Config, isLocalImage func(imageName strin
 
 	return &cache{
 		artifactCache:      artifactCache,
+		hashByName:         hashByName,
 		artifactGraph:      graph,
 		artifactStore:      store,
 		client:             client,
diff --git a/pkg/skaffold/build/cache/retrieve.go b/pkg/skaffold/build/cache/retrieve.go
index f7c816426..df1221343 100644
--- a/pkg/skaffold/build/cache/retrieve.go
+++ b/pkg/skaffold/build/cache/retrieve.go
@@ -57,7 +57,6 @@ func (c *cache) Build(ctx context.Context, out io.Writer, tags tag.ImageTags, ar
 	case results = <-lookup:
 	}
 
-	hashByName := make(map[string]string)
 	var needToBuild []*latest.Artifact
 	var alreadyBuilt []graph.Artifact
 	for i, artifact := range artifacts {
@@ -75,7 +74,7 @@ func (c *cache) Build(ctx context.Context, out io.Writer, tags tag.ImageTags, ar
 		case needsBuilding:
 			eventV2.CacheCheckMiss(artifact.ImageName, platforms.GetPlatforms(artifact.ImageName).String())
 			output.Yellow.Fprintln(out, "Not found. Building")
-			hashByName[artifact.ImageName] = result.Hash()
+			c.hashByName[artifact.ImageName] = result.Hash()
 			needToBuild = append(needToBuild, artifact)
 			continue
 
@@ -141,22 +140,19 @@ func (c *cache) Build(ctx context.Context, out io.Writer, tags tag.ImageTags, ar
 
 	log.Entry(ctx).Infoln("Cache check completed in", timeutil.Humanize(time.Since(start)))
 
+	defer func() {
+		if err := saveArtifactCache(c.cacheFile, c.artifactCache); err != nil {
+			log.Entry(ctx).Warnf("error saving cache file; caching may not work as expected: %v", err)
+		}
+	}()
+
 	bRes, err := buildAndTest(ctx, out, tags, needToBuild, platforms)
+
 	if err != nil {
 		endTrace(instrumentation.TraceEndError(err))
 		return nil, err
 	}
 
-	if err := c.addArtifacts(ctx, bRes, hashByName); err != nil {
-		log.Entry(ctx).Warnf("error adding artifacts to cache; caching may not work as expected: %v", err)
-		return append(bRes, alreadyBuilt...), nil
-	}
-
-	if err := saveArtifactCache(c.cacheFile, c.artifactCache); err != nil {
-		log.Entry(ctx).Warnf("error saving cache file; caching may not work as expected: %v", err)
-		return append(bRes, alreadyBuilt...), nil
-	}
-
 	return maintainArtifactOrder(append(bRes, alreadyBuilt...), artifacts), err
 }
 
@@ -175,32 +171,31 @@ func maintainArtifactOrder(built []graph.Artifact, artifacts []*latest.Artifact)
 	return ordered
 }
 
-func (c *cache) addArtifacts(ctx context.Context, bRes []graph.Artifact, hashByName map[string]string) error {
-	for _, a := range bRes {
-		entry := ImageDetails{}
-		isLocal, err := c.isLocalImage(a.ImageName)
+func (c *cache) AddArtifact(ctx context.Context, a graph.Artifact) error {
+	entry := ImageDetails{}
+	isLocal, err := c.isLocalImage(a.ImageName)
+	if err != nil {
+		return err
+	}
+	if isLocal {
+		imageID, err := c.client.ImageID(ctx, a.Tag)
 		if err != nil {
 			return err
 		}
-		if isLocal {
-			imageID, err := c.client.ImageID(ctx, a.Tag)
-			if err != nil {
-				return err
-			}
 
-			if imageID != "" {
-				entry.ID = imageID
-			}
-		} else {
-			ref, err := docker.ParseReference(a.Tag)
-			if err != nil {
-				return fmt.Errorf("parsing reference %q: %w", a.Tag, err)
-			}
-			entry.Digest = ref.Digest
+		if imageID != "" {
+			entry.ID = imageID
+		}
+	} else {
+		ref, err := docker.ParseReference(a.Tag)
+		if err != nil {
+			return fmt.Errorf("parsing reference %q: %w", a.Tag, err)
 		}
-		c.cacheMutex.Lock()
-		c.artifactCache[hashByName[a.ImageName]] = entry
-		c.cacheMutex.Unlock()
+		entry.Digest = ref.Digest
 	}
+	c.cacheMutex.Lock()
+	c.artifactCache[c.hashByName[a.ImageName]] = entry
+	c.cacheMutex.Unlock()
+
 	return nil
 }
diff --git a/pkg/skaffold/build/cache/types.go b/pkg/skaffold/build/cache/types.go
index 276c106ee..1cd28e9ea 100644
--- a/pkg/skaffold/build/cache/types.go
+++ b/pkg/skaffold/build/cache/types.go
@@ -30,6 +30,7 @@ type BuildAndTestFn func(context.Context, io.Writer, tag.ImageTags, []*latest.Ar
 
 type Cache interface {
 	Build(context.Context, io.Writer, tag.ImageTags, []*latest.Artifact, platform.Resolver, BuildAndTestFn) ([]graph.Artifact, error)
+	AddArtifact(ctx context.Context, a graph.Artifact) error
 }
 
 type noCache struct{}
@@ -37,3 +38,8 @@ type noCache struct{}
 func (n *noCache) Build(ctx context.Context, out io.Writer, tags tag.ImageTags, artifacts []*latest.Artifact, platforms platform.Resolver, buildAndTest BuildAndTestFn) ([]graph.Artifact, error) {
 	return buildAndTest(ctx, out, tags, artifacts, platforms)
 }
+
+func (n *noCache) AddArtifact(ctx context.Context, a graph.Artifact) error {
+	// noop
+	return nil
+}
diff --git a/pkg/skaffold/docker/dependencies.go b/pkg/skaffold/docker/dependencies.go
index 5cef07446..9ce5013d6 100644
--- a/pkg/skaffold/docker/dependencies.go
+++ b/pkg/skaffold/docker/dependencies.go
@@ -251,7 +251,7 @@ func readDockerignore(workspace string, absDockerfilePath string) ([]string, err
 }
 
 // WalkWorkspace walks the given host directories and records all files found.
-// Note: if you change this function, you might also want to modify `walkWorkspaceWithDestinations`.
+// Note: if you change this function, you might also want to modify walkWorkspaceWithDestinations.
 func WalkWorkspace(workspace string, excludes, deps []string) (map[string]bool, error) {
 	dockerIgnored, err := NewDockerIgnorePredicate(workspace, excludes)
 	if err != nil {
@@ -264,9 +264,9 @@ func WalkWorkspace(workspace string, excludes, deps []string) (map[string]bool,
 		absFrom := filepath.Join(workspace, dep)
 
 		keepFile := func(path string, info walk.Dirent) (bool, error) {
-			// Always keep root folders.
-			if info.IsDir() && path == absFrom {
-				return true, nil
+			// Ignore non empty dirs
+			if info.IsDir() && !util.IsEmptyDir(path) {
+				return false, nil
 			}
 
 			ignored, err := dockerIgnored(path, info)
@@ -277,7 +277,7 @@ func WalkWorkspace(workspace string, excludes, deps []string) (map[string]bool,
 			return !ignored, nil
 		}
 
-		if err := walk.From(absFrom).Unsorted().When(keepFile).WhenIsFile().Do(func(path string, info walk.Dirent) error {
+		if err := walk.From(absFrom).Unsorted().When(keepFile).Do(func(path string, info walk.Dirent) error {
 			relPath, err := filepath.Rel(workspace, path)
 			if err != nil {
 				return err
@@ -289,6 +289,5 @@ func WalkWorkspace(workspace string, excludes, deps []string) (map[string]bool,
 			return nil, fmt.Errorf("walking %q: %w", absFrom, err)
 		}
 	}
-
 	return files, nil
 }
diff --git a/pkg/skaffold/docker/syncmap.go b/pkg/skaffold/docker/syncmap.go
index 0d4f0601a..3cb9032e3 100644
--- a/pkg/skaffold/docker/syncmap.go
+++ b/pkg/skaffold/docker/syncmap.go
@@ -23,6 +23,7 @@ import (
 	"path"
 	"path/filepath"
 
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/walk"
 )
 
@@ -76,9 +77,9 @@ func walkWorkspaceWithDestinations(workspace string, excludes []string, fts []Fr
 		switch mode := fi.Mode(); {
 		case mode.IsDir():
 			keepFile := func(path string, info walk.Dirent) (bool, error) {
-				// Always keep root folders.
-				if info.IsDir() && path == absFrom {
-					return true, nil
+				// Ignore non empty dirs
+				if info.IsDir() && !util.IsEmptyDir(path) {
+					return false, nil
 				}
 
 				ignored, err := dockerIgnored(path, info)
diff --git a/pkg/skaffold/kubernetes/manifest/errors.go b/pkg/skaffold/kubernetes/manifest/errors.go
index 7a60a3faa..59096f644 100644
--- a/pkg/skaffold/kubernetes/manifest/errors.go
+++ b/pkg/skaffold/kubernetes/manifest/errors.go
@@ -75,7 +75,7 @@ func nsSettingErr(err error) error {
 }
 
 func nsAlreadySetErr() error {
-	err := fmt.Errorf("namespace already set in the namesapce")
+	err := fmt.Errorf("namespace field already set in the manifests")
 	return sErrors.NewError(err,
 		&proto.ActionableErr{
 			Message: err.Error(),
diff --git a/pkg/skaffold/runner/v2/new.go b/pkg/skaffold/runner/v2/new.go
index 681d044bc..cc3b69367 100644
--- a/pkg/skaffold/runner/v2/new.go
+++ b/pkg/skaffold/runner/v2/new.go
@@ -105,17 +105,6 @@ func NewForConfig(ctx context.Context, runCtx *runcontext.RunContext) (*Skaffold
 		return nil, fmt.Errorf("creating verifier: %w", err)
 	}
 
-	// The Builder must be instantiated AFTER the Deployer, because the Deploy target influences
-	// the Cluster object on the RunContext, which in turn influences whether or not we will push images.
-	var builder build.Builder
-	builder, err = build.NewBuilderMux(runCtx, store, func(p latest.Pipeline) (build.PipelineBuilder, error) {
-		return runner.GetBuilder(ctx, runCtx, store, sourceDependencies, p)
-	})
-	if err != nil {
-		endTrace(instrumentation.TraceEndError(err))
-		return nil, fmt.Errorf("creating builder: %w", err)
-	}
-
 	depLister := func(ctx context.Context, artifact *latest.Artifact) ([]string, error) {
 		ctx, endTrace := instrumentation.StartTrace(ctx, "NewForConfig_depLister")
 		defer endTrace()
@@ -139,6 +128,16 @@ func NewForConfig(ctx context.Context, runCtx *runcontext.RunContext) (*Skaffold
 		endTrace(instrumentation.TraceEndError(err))
 		return nil, fmt.Errorf("initializing cache: %w", err)
 	}
+	// The Builder must be instantiated AFTER the Deployer, because the Deploy target influences
+	// the Cluster object on the RunContext, which in turn influences whether or not we will push images.
+	var builder build.Builder
+	builder, err = build.NewBuilderMux(runCtx, store, artifactCache, func(p latest.Pipeline) (build.PipelineBuilder, error) {
+		return runner.GetBuilder(ctx, runCtx, store, sourceDependencies, p)
+	})
+	if err != nil {
+		endTrace(instrumentation.TraceEndError(err))
+		return nil, fmt.Errorf("creating builder: %w", err)
+	}
 
 	builder, tester, renderer, deployer = runner.WithTimings(builder, tester, renderer, deployer, runCtx.CacheArtifacts())
 	if runCtx.Notification() {
diff --git a/pkg/skaffold/util/util.go b/pkg/skaffold/util/util.go
index baeb49d8f..98dd26799 100644
--- a/pkg/skaffold/util/util.go
+++ b/pkg/skaffold/util/util.go
@@ -23,6 +23,7 @@ import (
 	"crypto/rand"
 	"encoding/json"
 	"fmt"
+	"io"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -287,6 +288,19 @@ func IsDir(path string) bool {
 	return (err == nil || !os.IsNotExist(err)) && info.IsDir()
 }
 
+// IsEmptyDir returns true for empty directories otherwise false
+func IsEmptyDir(path string) bool {
+	d, err := os.Open(path)
+	if err != nil {
+		return false
+	}
+	defer d.Close()
+	if _, err := d.ReadDir(1); err == io.EOF {
+		return true
+	}
+	return false
+}
+
 // IsHiddenDir returns if a directory is hidden.
 func IsHiddenDir(filename string) bool {
 	// Return false for current dir


Test patch:
diff --git a/integration/cache_test.go b/integration/cache_test.go
index 882af4f93..1b0cd3449 100644
--- a/integration/cache_test.go
+++ b/integration/cache_test.go
@@ -17,6 +17,9 @@ limitations under the License.
 package integration
 
 import (
+	"fmt"
+	"os"
+	"path"
 	"testing"
 	"time"
 
@@ -65,3 +68,22 @@ func TestCacheHits(t *testing.T) {
 func waitForEvent(t *testing.T, entries chan *proto.LogEntry, condition func(*proto.LogEntry) bool) {
 	failNowIfError(t, wait.PollImmediate(time.Millisecond*500, 2*time.Minute, func() (bool, error) { return condition(<-entries), nil }))
 }
+
+func TestCacheIfBuildFail(t *testing.T) {
+	MarkIntegrationTest(t, CanRunWithoutGcp)
+
+	ns, _ := SetupNamespace(t)
+
+	cacheFile := "cache_" + ns.Name
+	testDir := "testdata/cache"
+	relativePath := path.Join(testDir, cacheFile)
+	defer os.Remove(relativePath)
+
+	skaffold.Build("--cache-file", cacheFile).InDir(testDir).InNs(ns.Name).Run(t)
+
+	fInfo, err := os.Stat(relativePath)
+	failNowIfError(t, err)
+	if b := fInfo.Size(); b == 0 {
+		failNowIfError(t, fmt.Errorf("expected to see content in the cache file, saw %d bytes", b))
+	}
+}
diff --git a/integration/run_test.go b/integration/run_test.go
index b623c53ff..7a09b32c4 100644
--- a/integration/run_test.go
+++ b/integration/run_test.go
@@ -18,6 +18,7 @@ package integration
 
 import (
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
@@ -28,6 +29,10 @@ import (
 	"github.com/GoogleContainerTools/skaffold/testutil"
 )
 
+const (
+	emptydir = "testdata/empty-dir"
+)
+
 // Note: `custom-buildx` is not included as it depends on having a
 // `skaffold-builder` builder configured and a registry to push to.
 // TODO: remove nolint once we've reenabled integration tests
@@ -42,14 +47,14 @@ var tests = []struct {
 	targetLog   string
 }{
 	{
-		description: "copying directory",
-		dir:         "examples/getting-started",
-		pods:        []string{"getting-started"},
+		description: "copying-empty-directory",
+		dir:         emptydir,
+		pods:        []string{"empty-dir"},
 		targetLog:   "Hello world!",
 	},
 	{
 		description: "getting-started",
-		dir:         "testdata/getting-started",
+		dir:         "examples/getting-started",
 		pods:        []string{"getting-started"},
 		targetLog:   "Hello world!",
 	},
@@ -172,8 +177,14 @@ func TestRun(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.description, func(t *testing.T) {
 			ns, client := SetupNamespace(t)
-
 			args := append(test.args, "--cache-artifacts=false")
+			if test.dir == emptydir {
+				err := os.MkdirAll(filepath.Join(test.dir, "emptydir"), 0755)
+				t.Log("Creating empty directory")
+				if err != nil {
+					t.Errorf("Error creating empty dir: %s", err)
+				}
+			}
 			skaffold.Run(args...).InDir(test.dir).InNs(ns.Name).WithEnv(test.env).RunOrFail(t)
 
 			client.WaitForPodsReady(test.pods...)
@@ -192,6 +203,13 @@ func TestRunTail(t *testing.T) {
 			if test.targetLog == "" {
 				t.SkipNow()
 			}
+			if test.dir == emptydir {
+				err := os.MkdirAll(filepath.Join(test.dir, "emptydir"), 0755)
+				t.Log("Creating empty directory")
+				if err != nil {
+					t.Errorf("Error creating empty dir: %s", err)
+				}
+			}
 			ns, _ := SetupNamespace(t)
 
 			args := append(test.args, "--tail")
@@ -212,6 +230,13 @@ func TestRunTailDefaultNamespace(t *testing.T) {
 			if test.targetLog == "" {
 				t.SkipNow()
 			}
+			if test.dir == emptydir {
+				err := os.MkdirAll(filepath.Join(test.dir, "emptydir"), 0755)
+				t.Log("Creating empty directory")
+				if err != nil {
+					t.Errorf("Error creating empty dir: %s", err)
+				}
+			}
 
 			args := append(test.args, "--tail")
 			out := skaffold.Run(args...).InDir(test.dir).WithEnv(test.env).RunLive(t)
diff --git a/integration/testdata/cache/app/Dockerfile b/integration/testdata/cache/app/Dockerfile
new file mode 100644
index 000000000..df0d88271
--- /dev/null
+++ b/integration/testdata/cache/app/Dockerfile
@@ -0,0 +1,16 @@
+ARG BASE
+FROM golang:1.15 as builder
+COPY main.go .
+# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
+ARG SKAFFOLD_GO_GCFLAGS
+RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /app .
+
+FROM $BASE
+# Define GOTRACEBACK to mark this container as using the Go language runtime
+# for `skaffold debug` (https://skaffold.dev/docs/workflows/debug/).
+ENV GOTRACEBACK=single
+CMD ["./app"]
+COPY --from=builder /app .
+
+# intended to cause failure for test case
+RUN
\ No newline at end of file
diff --git a/integration/testdata/cache/app/k8s-pod.yaml b/integration/testdata/cache/app/k8s-pod.yaml
new file mode 100644
index 000000000..62e06f629
--- /dev/null
+++ b/integration/testdata/cache/app/k8s-pod.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: simple-artifact-dependency-app
+spec:
+  containers:
+  - name: simple-artifact-dependency-app
+    image: app
diff --git a/integration/testdata/cache/app/main.go b/integration/testdata/cache/app/main.go
new file mode 100644
index 000000000..700ab4591
--- /dev/null
+++ b/integration/testdata/cache/app/main.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"time"
+)
+
+func main() {
+	dat, err := ioutil.ReadFile("hello.txt")
+	if err != nil {
+		panic(err)
+	}
+
+	for {
+		fmt.Println(string(dat))
+		time.Sleep(time.Second * 1)
+	}
+}
diff --git a/integration/testdata/cache/base/Dockerfile b/integration/testdata/cache/base/Dockerfile
new file mode 100644
index 000000000..02480fbb9
--- /dev/null
+++ b/integration/testdata/cache/base/Dockerfile
@@ -0,0 +1,4 @@
+FROM alpine:3
+COPY hello.txt .
+CMD ["./app"]
+
diff --git a/integration/testdata/cache/base/hello.txt b/integration/testdata/cache/base/hello.txt
new file mode 100644
index 000000000..5e1c309da
--- /dev/null
+++ b/integration/testdata/cache/base/hello.txt
@@ -0,0 +1,1 @@
+Hello World
\ No newline at end of file
diff --git a/integration/testdata/cache/skaffold.yaml b/integration/testdata/cache/skaffold.yaml
new file mode 100644
index 000000000..7a6b6abfb
--- /dev/null
+++ b/integration/testdata/cache/skaffold.yaml
@@ -0,0 +1,16 @@
+apiVersion: skaffold/v2beta28
+kind: Config
+build:
+  artifacts:
+  - image: app
+    context: app
+    requires:
+    - image: base
+      alias: BASE
+  - image: base
+    context: base
+deploy:
+  kubectl:
+    manifests:
+    - app/k8s-pod.yaml
+
diff --git a/integration/testdata/empty-dir/Dockerfile b/integration/testdata/empty-dir/Dockerfile
new file mode 100644
index 000000000..cf73acf30
--- /dev/null
+++ b/integration/testdata/empty-dir/Dockerfile
@@ -0,0 +1,13 @@
+FROM golang:1.15 as builder
+COPY main.go .
+COPY emptydir .
+# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
+ARG SKAFFOLD_GO_GCFLAGS
+RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /app main.go
+
+FROM alpine:3
+# Define GOTRACEBACK to mark this container as using the Go language runtime
+# for `skaffold debug` (https://skaffold.dev/docs/workflows/debug/).
+ENV GOTRACEBACK=single
+CMD ["./app"]
+COPY --from=builder /app .
diff --git a/integration/testdata/empty-dir/README.md b/integration/testdata/empty-dir/README.md
new file mode 100644
index 000000000..efcec7466
--- /dev/null
+++ b/integration/testdata/empty-dir/README.md
@@ -0,0 +1,9 @@
+### Example: Getting started with a simple go app
+
+[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/GoogleContainerTools/skaffold&cloudshell_open_in_editor=README.md&cloudshell_workspace=examples/getting-started)
+
+This is a simple example based on:
+
+* **building** a single Go file app and with a multistage `Dockerfile` using local docker to build
+* **tagging** using the default tagPolicy (`gitCommit`)
+* **deploying** a single container pod using `kubectl`
\ No newline at end of file
diff --git a/integration/testdata/empty-dir/k8s-pod.yaml b/integration/testdata/empty-dir/k8s-pod.yaml
new file mode 100644
index 000000000..e86cc049f
--- /dev/null
+++ b/integration/testdata/empty-dir/k8s-pod.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: empty-dir
+spec:
+  containers:
+  - name: empty-dir
+    image: skaffold-example
diff --git a/integration/testdata/empty-dir/main.go b/integration/testdata/empty-dir/main.go
new file mode 100644
index 000000000..593721cfe
--- /dev/null
+++ b/integration/testdata/empty-dir/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	for {
+		fmt.Println("Hello world!")
+
+		time.Sleep(time.Second * 1)
+	}
+}
diff --git a/integration/testdata/empty-dir/skaffold.yaml b/integration/testdata/empty-dir/skaffold.yaml
new file mode 100644
index 000000000..7a42ccfa3
--- /dev/null
+++ b/integration/testdata/empty-dir/skaffold.yaml
@@ -0,0 +1,8 @@
+apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  artifacts:
+  - image: skaffold-example
+manifests:
+  rawYaml:
+    - k8s-*
diff --git a/integration/testdata/getting-started/Dockerfile b/integration/testdata/getting-started/Dockerfile
index 7936c8f6e..fb06046e6 100644
--- a/integration/testdata/getting-started/Dockerfile
+++ b/integration/testdata/getting-started/Dockerfile
@@ -1,6 +1,5 @@
 FROM golang:1.15 as builder
 COPY main.go .
-COPY testdir .
 
 # `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
 ARG SKAFFOLD_GO_GCFLAGS
diff --git a/pkg/skaffold/build/builder_mux_test.go b/pkg/skaffold/build/builder_mux_test.go
index 39371183f..d5ca2f31a 100644
--- a/pkg/skaffold/build/builder_mux_test.go
+++ b/pkg/skaffold/build/builder_mux_test.go
@@ -78,7 +78,7 @@ func TestNewBuilderMux(t *testing.T) {
 		testutil.Run(t, test.description, func(t *testutil.T) {
 			cfg := &mockConfig{pipelines: test.pipelines}
 
-			b, err := NewBuilderMux(cfg, nil, test.pipeBuilder)
+			b, err := NewBuilderMux(cfg, nil, nil, test.pipeBuilder)
 			t.CheckError(test.shouldErr, err)
 			if test.shouldErr {
 				return
diff --git a/pkg/skaffold/build/cache/retrieve_test.go b/pkg/skaffold/build/cache/retrieve_test.go
index b573c8432..48f86dffb 100644
--- a/pkg/skaffold/build/cache/retrieve_test.go
+++ b/pkg/skaffold/build/cache/retrieve_test.go
@@ -59,6 +59,7 @@ type mockBuilder struct {
 	push         bool
 	dockerDaemon docker.LocalDaemon
 	store        build.ArtifactStore
+	cache        Cache
 }
 
 func (b *mockBuilder) Build(ctx context.Context, out io.Writer, tags tag.ImageTags, artifacts []*latest.Artifact, _ platform.Resolver) ([]graph.Artifact, error) {
@@ -79,16 +80,21 @@ func (b *mockBuilder) Build(ctx context.Context, out io.Writer, tags tag.ImageTa
 				return nil, err
 			}
 
-			built = append(built, graph.Artifact{
+			a := graph.Artifact{
 				ImageName: artifact.ImageName,
 				Tag:       build.TagWithDigest(tag, digest),
-			})
+			}
+
+			built = append(built, a)
+			b.cache.AddArtifact(ctx, a)
 		} else {
-			built = append(built, graph.Artifact{
+			a := graph.Artifact{
 				ImageName: artifact.ImageName,
 				Tag:       tag,
-			})
+			}
+			built = append(built, a)
 			b.store.Record(artifact, tag)
+			b.cache.AddArtifact(ctx, a)
 		}
 	}
 
@@ -147,7 +153,7 @@ func TestCacheBuildLocal(t *testing.T) {
 		t.CheckNoError(err)
 
 		// First build: Need to build both artifacts
-		builder := &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store}
+		builder := &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store, cache: artifactCache}
 		bRes, err := artifactCache.Build(context.Background(), io.Discard, tags, artifacts, platform.Resolver{}, builder.Build)
 
 		t.CheckNoError(err)
@@ -156,7 +162,7 @@ func TestCacheBuildLocal(t *testing.T) {
 
 		// Second build: both artifacts are read from cache
 		// Artifacts should always be returned in their original order
-		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store}
+		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store, cache: artifactCache}
 		bRes, err = artifactCache.Build(context.Background(), io.Discard, tags, artifacts, platform.Resolver{}, builder.Build)
 
 		t.CheckNoError(err)
@@ -168,7 +174,7 @@ func TestCacheBuildLocal(t *testing.T) {
 		// Third build: change first artifact's dependency
 		// Artifacts should always be returned in their original order
 		tmpDir.Write("dep1", "new content")
-		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store}
+		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store, cache: artifactCache}
 		bRes, err = artifactCache.Build(context.Background(), io.Discard, tags, artifacts, platform.Resolver{}, builder.Build)
 
 		t.CheckNoError(err)
@@ -180,7 +186,7 @@ func TestCacheBuildLocal(t *testing.T) {
 		// Fourth build: change second artifact's dependency
 		// Artifacts should always be returned in their original order
 		tmpDir.Write("dep3", "new content")
-		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store}
+		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store, cache: artifactCache}
 		bRes, err = artifactCache.Build(context.Background(), io.Discard, tags, artifacts, platform.Resolver{}, builder.Build)
 
 		t.CheckNoError(err)
@@ -243,7 +249,7 @@ func TestCacheBuildRemote(t *testing.T) {
 		t.CheckNoError(err)
 
 		// First build: Need to build both artifacts
-		builder := &mockBuilder{dockerDaemon: dockerDaemon, push: true}
+		builder := &mockBuilder{dockerDaemon: dockerDaemon, push: true, cache: artifactCache}
 		bRes, err := artifactCache.Build(context.Background(), io.Discard, tags, artifacts, platform.Resolver{}, builder.Build)
 
 		t.CheckNoError(err)
@@ -254,7 +260,7 @@ func TestCacheBuildRemote(t *testing.T) {
 		t.CheckDeepEqual("artifact2", bRes[1].ImageName)
 
 		// Second build: both artifacts are read from cache
-		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: true}
+		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: true, cache: artifactCache}
 		bRes, err = artifactCache.Build(context.Background(), io.Discard, tags, artifacts, platform.Resolver{}, builder.Build)
 
 		t.CheckNoError(err)
@@ -265,7 +271,7 @@ func TestCacheBuildRemote(t *testing.T) {
 
 		// Third build: change one artifact's dependencies
 		tmpDir.Write("dep1", "new content")
-		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: true}
+		builder = &mockBuilder{dockerDaemon: dockerDaemon, push: true, cache: artifactCache}
 		bRes, err = artifactCache.Build(context.Background(), io.Discard, tags, artifacts, platform.Resolver{}, builder.Build)
 
 		t.CheckNoError(err)
diff --git a/pkg/skaffold/docker/dependencies_test.go b/pkg/skaffold/docker/dependencies_test.go
index 0df3eb893..bdd91f79e 100644
--- a/pkg/skaffold/docker/dependencies_test.go
+++ b/pkg/skaffold/docker/dependencies_test.go
@@ -30,6 +30,11 @@ import (
 	"github.com/GoogleContainerTools/skaffold/testutil"
 )
 
+const copyEmptydirectory = `
+FROM ubuntu:14.04
+COPY emptydir .
+`
+
 const copyServerGo = `
 FROM ubuntu:14.04
 COPY server.go .
@@ -283,6 +288,12 @@ func TestGetDependencies(t *testing.T) {
 		expected  []string
 		shouldErr bool
 	}{
+		{
+			description: "copy empty directory",
+			dockerfile:  copyEmptydirectory,
+			workspace:   ".",
+			expected:    []string{"Dockerfile", "emptydir"},
+		},
 		{
 			description: "buildkit dockerfile",
 			dockerfile:  buildKitDockerfile,
@@ -380,7 +391,7 @@ func TestGetDependencies(t *testing.T) {
 		{
 			description: "dockerignore test",
 			dockerfile:  copyDirectory,
-			ignore:      "bar\ndocker/*",
+			ignore:      "emptydir\nbar\ndocker/*",
 			workspace:   ".",
 			expected:    []string{".dot", "Dockerfile", "file", "server.go", "test.conf", "worker.go"},
 		},
@@ -402,40 +413,40 @@ func TestGetDependencies(t *testing.T) {
 			description: "ignore none",
 			dockerfile:  copyAll,
 			workspace:   ".",
-			expected:    []string{".dot", "Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "file", "server.go", "test.conf", "worker.go"},
+			expected:    []string{".dot", "Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "emptydir", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
 			description: "ignore dotfiles",
 			dockerfile:  copyAll,
 			workspace:   ".",
 			ignore:      ".*",
-			expected:    []string{"Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "file", "server.go", "test.conf", "worker.go"},
+			expected:    []string{"Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "emptydir", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
 			description: "ignore dotfiles (root syntax)",
 			dockerfile:  copyAll,
 			workspace:   ".",
 			ignore:      "/.*",
-			expected:    []string{"Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "file", "server.go", "test.conf", "worker.go"},
+			expected:    []string{"Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "emptydir", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
 			description: "dockerignore with context in parent directory",
 			dockerfile:  copyDirectory,
 			workspace:   "docker/..",
-			ignore:      "bar\ndocker\n*.go",
+			ignore:      "emptydir\nbar\ndocker\n*.go",
 			expected:    []string{".dot", "Dockerfile", "file", "test.conf"},
 		},
 		{
 			description: "onbuild test",
 			dockerfile:  onbuild,
 			workspace:   ".",
-			expected:    []string{".dot", "Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "file", "server.go", "test.conf", "worker.go"},
+			expected:    []string{".dot", "Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "emptydir", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
 			description: "onbuild with dockerignore",
 			dockerfile:  onbuild,
 			workspace:   ".",
-			ignore:      "bar\ndocker/*",
+			ignore:      "emptydir\nbar\ndocker/*",
 			expected:    []string{".dot", "Dockerfile", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
@@ -579,7 +590,7 @@ func TestGetDependencies(t *testing.T) {
 			description:    "find specific dockerignore",
 			dockerfile:     copyDirectory,
 			workspace:      ".",
-			ignore:         "bar\ndocker/*",
+			ignore:         "emptydir\nbar\ndocker/*",
 			ignoreFilename: "Dockerfile.dockerignore",
 			expected:       []string{".dot", "Dockerfile", "Dockerfile.dockerignore", "file", "server.go", "test.conf", "worker.go"},
 		},
@@ -605,6 +616,7 @@ func TestGetDependencies(t *testing.T) {
 
 			tmpDir := t.NewTempDir().
 				Touch("docker/nginx.conf", "docker/bar", "server.go", "test.conf", "worker.go", "bar", "file", ".dot")
+			tmpDir.Mkdir("emptydir")
 			if test.dockerfile != "" {
 				tmpDir.Write(test.workspace+"/Dockerfile", test.dockerfile)
 			}
@@ -622,7 +634,6 @@ func TestGetDependencies(t *testing.T) {
 				mode: config.RunModes.Dev,
 			}
 			deps, err := GetDependencies(context.Background(), NewBuildConfig(workspace, "test", "Dockerfile", test.buildArgs), m)
-
 			t.CheckError(test.shouldErr, err)
 			t.CheckDeepEqual(test.expected, deps)
 		})
diff --git a/pkg/skaffold/util/util_test.go b/pkg/skaffold/util/util_test.go
index 112175c22..4ecd1f173 100644
--- a/pkg/skaffold/util/util_test.go
+++ b/pkg/skaffold/util/util_test.go
@@ -449,3 +449,12 @@ func TestIsURL(t *testing.T) {
 func stringPointer(s string) *string {
 	return &s
 }
+
+func TestIsEmptyDir(t *testing.T) {
+	tmpDir := testutil.NewTempDir(t).Touch("file")
+	emptyTmpDir := testutil.NewTempDir(t)
+
+	testutil.CheckDeepEqual(t, true, IsEmptyDir(emptyTmpDir.Root()))
+	testutil.CheckDeepEqual(t, false, IsEmptyDir(tmpDir.Root()))
+	testutil.CheckDeepEqual(t, false, IsEmptyDir(filepath.Join(tmpDir.Root(), "file")))
+}