diff --git a/.github/workflows/ports_unix.yml b/.github/workflows/ports_unix.yml
index 63cc1c0faa9219e8743827f7a1594cf0cfb1e0ee..ab2406647a8ca919d7b722278e57028f5970c917 100644
--- a/.github/workflows/ports_unix.yml
+++ b/.github/workflows/ports_unix.yml
@@ -35,7 +35,7 @@ jobs:
       env:
         SOURCE_DATE_EPOCH: 1234567890
     - name: Check reproducible build date
-      run: echo | ports/unix/micropython-minimal -i | grep 'on 2009-02-13;'
+      run: echo | ports/unix/build-minimal/micropython-minimal -i | grep 'on 2009-02-13;'
 
   standard:
     runs-on: ubuntu-latest
diff --git a/examples/embedding/Makefile b/examples/embedding/Makefile
index 99f239a7c5af8ad83e32b8dafacc5b5fad3bfc01..7de1219b2644225426921f7cd9c949748c4a6f81 100644
--- a/examples/embedding/Makefile
+++ b/examples/embedding/Makefile
@@ -1,6 +1,6 @@
 MPTOP = ../..
 CFLAGS = -std=c99 -I. -I$(MPTOP) -DNO_QSTR
-LDFLAGS = -L.
+LDFLAGS = -L./build
 
 hello-embed: hello-embed.o -lmicropython
 
diff --git a/ports/unix/Makefile b/ports/unix/Makefile
index 3b339c3d3f79fc4ae2d33912140401061c3ea060..709cc79853d57c8b754f5bb7b8484294215c9f5d 100644
--- a/ports/unix/Makefile
+++ b/ports/unix/Makefile
@@ -304,18 +304,18 @@ include $(TOP)/py/mkrules.mk
 
 .PHONY: test test_full
 
-test: $(PROG) $(TOP)/tests/run-tests.py
+test: $(BUILD)/$(PROG) $(TOP)/tests/run-tests.py
 	$(eval DIRNAME=ports/$(notdir $(CURDIR)))
-	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py
+	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(BUILD)/$(PROG) ./run-tests.py
 
-test_full: $(PROG) $(TOP)/tests/run-tests.py
+test_full: $(BUILD)/$(PROG) $(TOP)/tests/run-tests.py
 	$(eval DIRNAME=ports/$(notdir $(CURDIR)))
-	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py
-	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py -d thread
-	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py --emit native
-	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py --via-mpy $(RUN_TESTS_MPY_CROSS_FLAGS) -d basics float micropython
-	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py --via-mpy $(RUN_TESTS_MPY_CROSS_FLAGS) --emit native -d basics float micropython
-	cat $(TOP)/tests/basics/0prelim.py | ./$(PROG) | grep -q 'abc'
+	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(BUILD)/$(PROG) ./run-tests.py
+	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(BUILD)/$(PROG) ./run-tests.py -d thread
+	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(BUILD)/$(PROG) ./run-tests.py --emit native
+	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(BUILD)/$(PROG) ./run-tests.py --via-mpy $(RUN_TESTS_MPY_CROSS_FLAGS) -d basics float micropython
+	cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(BUILD)/$(PROG) ./run-tests.py --via-mpy $(RUN_TESTS_MPY_CROSS_FLAGS) --emit native -d basics float micropython
+	cat $(TOP)/tests/basics/0prelim.py | ./$(BUILD)/$(PROG) | grep -q 'abc'
 
 test_gcov: test_full
 	gcov -o $(BUILD)/py $(TOP)/py/*.c
@@ -346,9 +346,9 @@ $(BUILD)/lib/libffi/include/ffi.h: $(TOP)/lib/libffi/configure
 PREFIX = /usr/local
 BINDIR = $(DESTDIR)$(PREFIX)/bin
 
-install: $(PROG)
+install: $(BUILD)/$(PROG)
 	install -d $(BINDIR)
-	install $(PROG) $(BINDIR)/$(PROG)
+	install $(BUILD)/$(PROG) $(BINDIR)/$(PROG)
 
 uninstall:
 	-rm $(BINDIR)/$(PROG)
diff --git a/py/dynruntime.mk b/py/dynruntime.mk
index 09cbb2dd37cbb5f8d811b712b51b4b2117a0103c..10feefc4a7c22d4720c6fd3476d1ced8de9bfa28 100644
--- a/py/dynruntime.mk
+++ b/py/dynruntime.mk
@@ -7,7 +7,7 @@ ECHO = @echo
 RM = /bin/rm
 MKDIR = /bin/mkdir
 PYTHON = python3
-MPY_CROSS = $(MPY_DIR)/mpy-cross/mpy-cross
+MPY_CROSS = $(MPY_DIR)/mpy-cross/build/mpy-cross
 MPY_TOOL = $(PYTHON) $(MPY_DIR)/tools/mpy-tool.py
 MPY_LD = $(PYTHON) $(MPY_DIR)/tools/mpy_ld.py
 
diff --git a/py/mkenv.mk b/py/mkenv.mk
index cc04a8c0b30714a164da2bea720604a786a745c2..ea2e34f3b64dee60f0afc43f773c5b4e4b308049 100644
--- a/py/mkenv.mk
+++ b/py/mkenv.mk
@@ -61,7 +61,7 @@ MPY_LIB_SUBMODULE_DIR = $(TOP)/lib/micropython-lib
 MPY_LIB_DIR = $(MPY_LIB_SUBMODULE_DIR)
 
 ifeq ($(MICROPY_MPYCROSS),)
-MICROPY_MPYCROSS = $(TOP)/mpy-cross/mpy-cross
+MICROPY_MPYCROSS = $(TOP)/mpy-cross/build/mpy-cross
 MICROPY_MPYCROSS_DEPENDENCY = $(MICROPY_MPYCROSS)
 endif
 
diff --git a/py/mkrules.cmake b/py/mkrules.cmake
index 2f168ede6f2b4f4dc48eb6aca0989f22a0b8a9c9..e7c4101ddb72d8d097b3ca1e17da9d4334760667 100644
--- a/py/mkrules.cmake
+++ b/py/mkrules.cmake
@@ -193,7 +193,7 @@ if(MICROPY_FROZEN_MANIFEST)
     # to automatically build mpy-cross if needed.
     set(MICROPY_MPYCROSS $ENV{MICROPY_MPYCROSS})
     if(NOT MICROPY_MPYCROSS)
-        set(MICROPY_MPYCROSS_DEPENDENCY ${MICROPY_DIR}/mpy-cross/mpy-cross)
+        set(MICROPY_MPYCROSS_DEPENDENCY ${MICROPY_DIR}/mpy-cross/build/mpy-cross)
         if(NOT MICROPY_MAKE_EXECUTABLE)
             set(MICROPY_MAKE_EXECUTABLE make)
         endif()
diff --git a/py/mkrules.mk b/py/mkrules.mk
index af116786798a96e8e1217675b7363f77d68fb4a2..73c33227c09afd2c40b58afe2323bee7e757248b 100644
--- a/py/mkrules.mk
+++ b/py/mkrules.mk
@@ -162,7 +162,7 @@ $(HEADER_BUILD):
 ifneq ($(MICROPY_MPYCROSS_DEPENDENCY),)
 # to automatically build mpy-cross, if needed
 $(MICROPY_MPYCROSS_DEPENDENCY):
-	$(MAKE) -C $(dir $@)
+	$(MAKE) -C $(dir $@)..
 endif
 
 ifneq ($(FROZEN_DIR),)
diff --git a/tests/run-natmodtests.py b/tests/run-natmodtests.py
index 8eb27169c4ca1372e60906626b66f9bd4a6f1eaf..9130e00d6eba1780c1d92f5ea1ddf6dad1c42cb5 100755
--- a/tests/run-natmodtests.py
+++ b/tests/run-natmodtests.py
@@ -14,7 +14,7 @@ import pyboard
 
 # Paths for host executables
 CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3")
-MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", "../ports/unix/micropython-coverage")
+MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", "../ports/unix/build-coverage/micropython-coverage")
 
 NATMOD_EXAMPLE_DIR = "../examples/natmod/"
 
diff --git a/tests/run-tests.py b/tests/run-tests.py
index 2745ee1393badff868b5df9d5f563df6c6d83367..8e9bd843134fce8dd81f3c09566aa41030fccdaa 100755
--- a/tests/run-tests.py
+++ b/tests/run-tests.py
@@ -33,14 +33,16 @@ if os.name == "nt":
     MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", base_path("../ports/windows/micropython.exe"))
 else:
     CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3")
-    MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", base_path("../ports/unix/micropython"))
+    MICROPYTHON = os.getenv(
+        "MICROPY_MICROPYTHON", base_path("../ports/unix/build-standard/micropython")
+    )
 
 # Use CPython options to not save .pyc files, to only access the core standard library
 # (not site packages which may clash with u-module names), and improve start up time.
 CPYTHON3_CMD = [CPYTHON3, "-BS"]
 
 # mpy-cross is only needed if --via-mpy command-line arg is passed
-MPYCROSS = os.getenv("MICROPY_MPYCROSS", base_path("../mpy-cross/mpy-cross"))
+MPYCROSS = os.getenv("MICROPY_MPYCROSS", base_path("../mpy-cross/build/mpy-cross"))
 
 # For diff'ing test output
 DIFF = os.getenv("MICROPY_DIFF", "diff -u")
diff --git a/tools/ci.sh b/tools/ci.sh
index 7e2479e43d388ffcf330f6f956d806fe09065ec0..20119342cfcf57975f16bc1afcfb578af092a334 100755
--- a/tools/ci.sh
+++ b/tools/ci.sh
@@ -409,9 +409,9 @@ function ci_unix_run_tests_full_helper {
     variant=$1
     shift
     if [ $variant = standard ]; then
-        micropython=micropython
+        micropython=build-$variant/micropython
     else
-        micropython=micropython-$variant
+        micropython=build-$variant/micropython-$variant
     fi
     make -C ports/unix VARIANT=$variant "$@" test_full
     (cd tests && MICROPY_CPYTHON3=python3 MICROPY_MICROPYTHON=../ports/unix/$micropython ./run-multitests.py multi_net/*.py)
@@ -444,7 +444,7 @@ function ci_unix_minimal_build {
 }
 
 function ci_unix_minimal_run_tests {
-    (cd tests && MICROPY_CPYTHON3=python3 MICROPY_MICROPYTHON=../ports/unix/micropython-minimal ./run-tests.py -e exception_chain -e self_type_check -e subclass_native_init -d basics)
+    (cd tests && MICROPY_CPYTHON3=python3 MICROPY_MICROPYTHON=../ports/unix/build-minimal/micropython-minimal ./run-tests.py -e exception_chain -e self_type_check -e subclass_native_init -d basics)
 }
 
 function ci_unix_standard_build {
@@ -491,21 +491,21 @@ function ci_unix_coverage_run_mpy_merge_tests {
         test=$(basename $inpy .py)
         echo $test
         outmpy=$outdir/$test.mpy
-        $mptop/mpy-cross/mpy-cross -o $outmpy $inpy
-        (cd $outdir && $mptop/ports/unix/micropython-coverage -m $test >> out-individual)
+        $mptop/mpy-cross/build/mpy-cross -o $outmpy $inpy
+        (cd $outdir && $mptop/ports/unix/build-coverage/micropython-coverage -m $test >> out-individual)
         allmpy+=($outmpy)
     done
 
     # Merge all the tests into one .mpy file, and then execute it.
     python3 $mptop/tools/mpy-tool.py --merge -o $outdir/merged.mpy ${allmpy[@]}
-    (cd $outdir && $mptop/ports/unix/micropython-coverage -m merged > out-merged)
+    (cd $outdir && $mptop/ports/unix/build-coverage/micropython-coverage -m merged > out-merged)
 
     # Make sure the outputs match.
     diff $outdir/out-individual $outdir/out-merged && /bin/rm -rf $outdir
 }
 
 function ci_unix_coverage_run_native_mpy_tests {
-    MICROPYPATH=examples/natmod/features2 ./ports/unix/micropython-coverage -m features2
+    MICROPYPATH=examples/natmod/features2 ./ports/unix/build-coverage/micropython-coverage -m features2
     (cd tests && ./run-natmodtests.py "$@" extmod/{btree*,framebuf*,uheapq*,urandom*,ure*,uzlib*}.py)
 }
 
@@ -614,7 +614,7 @@ function ci_unix_macos_run_tests {
     # Issues with macOS tests:
     # - import_pkg7 has a problem with relative imports
     # - urandom_basic has a problem with getrandbits(0)
-    (cd tests && ./run-tests.py --exclude 'import_pkg7.py' --exclude 'urandom_basic.py')
+    (cd tests && MICROPY_MICROPYTHON=../ports/unix/build-standard/micropython ./run-tests.py --exclude 'import_pkg7.py' --exclude 'urandom_basic.py')
 }
 
 function ci_unix_qemu_mips_setup {
@@ -634,7 +634,7 @@ function ci_unix_qemu_mips_run_tests {
     # - (i)listdir does not work, it always returns the empty list (it's an issue with the underlying C call)
     # - ffi tests do not work
     file ./ports/unix/micropython-coverage
-    (cd tests && MICROPY_MICROPYTHON=../ports/unix/micropython-coverage ./run-tests.py --exclude 'vfs_posix.py' --exclude 'ffi_(callback|float|float2).py')
+    (cd tests && MICROPY_MICROPYTHON=../ports/unix/build-coverage/micropython-coverage ./run-tests.py --exclude 'vfs_posix.py' --exclude 'ffi_(callback|float|float2).py')
 }
 
 function ci_unix_qemu_arm_setup {
@@ -654,7 +654,7 @@ function ci_unix_qemu_arm_run_tests {
     # - (i)listdir does not work, it always returns the empty list (it's an issue with the underlying C call)
     export QEMU_LD_PREFIX=/usr/arm-linux-gnueabi
     file ./ports/unix/micropython-coverage
-    (cd tests && MICROPY_MICROPYTHON=../ports/unix/micropython-coverage ./run-tests.py --exclude 'vfs_posix.py')
+    (cd tests && MICROPY_MICROPYTHON=../ports/unix/build-coverage/micropython-coverage ./run-tests.py --exclude 'vfs_posix.py')
 }
 
 ########################################################################################
diff --git a/tools/makemanifest.py b/tools/makemanifest.py
index 8cdc3eb7741b9b20c303cdadc5ed1054dd139e60..e69698d3f23404c76cf417cbc1529bfb4dcc9934 100644
--- a/tools/makemanifest.py
+++ b/tools/makemanifest.py
@@ -329,7 +329,7 @@ def main():
         sys.exit(1)
 
     # Get paths to tools
-    MPY_CROSS = VARS["MPY_DIR"] + "/mpy-cross/mpy-cross"
+    MPY_CROSS = VARS["MPY_DIR"] + "/mpy-cross/build/mpy-cross"
     if sys.platform == "win32":
         MPY_CROSS += ".exe"
     MPY_CROSS = os.getenv("MICROPY_MPYCROSS", MPY_CROSS)