diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 5cc61a3..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "external/pybind11_protobuf"]
-	path = external/pybind11_protobuf
-	url = https://github.com/srmainwaring/pybind11_protobuf.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 81f3f48..23f8b1d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,5 @@
-cmake_minimum_required(VERSION 3.10.2 FATAL_ERROR)
+cmake_minimum_required(VERSION 3.18)
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
 
 project(gz-python)
 
@@ -10,20 +11,20 @@ if(NOT APPLE)
 endif()
 
 #============================================================================
-# Find protobuf
-# 
-# macOS: $ brew install protobuf
-# 
-find_package(Protobuf REQUIRED)
+# Options
+option(BUILD_protobuf "Build protobuf Library" ON)
+message(STATUS "Build protobuf: ${BUILD_protobuf}")
+
+option(BUILD_pybind11_protobuf "Build pybind11_protobuf Library" ON)
+message(STATUS "Build pybind11_protobuf: ${BUILD_pybind11_protobuf}")
 
 #============================================================================
-# Find pybind11
+# Find Python
 # 
 # macOS: $ brew install python
 # macOS: $ brew install pybind11
 # 
 find_package(Python 3.10 EXACT COMPONENTS Interpreter Development)
-# find_package(pybind11 CONFIG)
 
 #============================================================================
 # Find Gazebo dependencies
@@ -44,9 +45,18 @@ find_package(gz-msgs${GZ_MSGS_VER} REQUIRED)
 find_package(gz-transport${GZ_TRANSPORT_VER} REQUIRED)
 
 #============================================================================
-# subdirectories for external dependencies
+# Build dependencies
+add_subdirectory(cmake/dependencies dependencies)
+list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_BINARY_DIR}/dependencies/install)
+
+include(deps)
+
+message("pybind11_protobuf_FOUND: ${pybind11_protobuf_FOUND}")
+message("pybind11_protobuf_SOURCE_DIR: ${pybind11_protobuf_SOURCE_DIR}")
+message("pybind11_protobuf_INCLUDE_DIRS: ${pybind11_protobuf_INCLUDE_DIRS}")
 
-add_subdirectory(external)
+# TODO fix upstream...
+set(pybind11_protobuf_INCLUDE_DIRS ${pybind11_protobuf_SOURCE_DIR})
 
 #============================================================================
 # gz_msgs_extras_lib C++ library
@@ -75,14 +85,14 @@ target_link_libraries(extras
   PRIVATE
     gz_msgs_extras_lib
     gz-msgs${GZ_MSGS_VER}::gz-msgs${GZ_MSGS_VER}
-    ${Protobuf_LIBRARY}
+    protobuf::libprotobuf
     pybind11_native_proto_caster
 )
 
 target_include_directories(extras
   PRIVATE
     ${PROJECT_SOURCE_DIR}/include
-    ${PROJECT_SOURCE_DIR}/external/pybind11_protobuf
+    ${pybind11_protobuf_INCLUDE_DIRS}
 )
 
 add_dependencies(extras
@@ -107,14 +117,14 @@ target_link_libraries(transport
   PRIVATE
     gz-msgs${GZ_MSGS_VER}::gz-msgs${GZ_MSGS_VER}
     gz-transport${GZ_TRANSPORT_VER}::gz-transport${GZ_TRANSPORT_VER}
-    ${Protobuf_LIBRARY}
+    protobuf::libprotobuf
     pybind11_native_proto_caster
 )
 
 target_include_directories(transport
   PRIVATE
-    ${Protobuf_INCLUDE_DIR}
-    ${PROJECT_SOURCE_DIR}/external/pybind11_protobuf
+    ${protobuf_INCLUDE_DIRS}
+    ${pybind11_protobuf_INCLUDE_DIRS}
 )
 
 set_target_properties(transport
diff --git a/README.md b/README.md
index bcc8480..451eb41 100644
--- a/README.md
+++ b/README.md
@@ -11,11 +11,11 @@ This project depends directly on [`gz-msgs`](https://github.com/gazebosim/gz-msg
 
 ### Install `gz-python`
 
-Clone this repo into the workspace source directory and update external submodules:
+Clone this repo into the workspace source directory:
 
 ```bash
 cd ~/gz_ws/src
-git clone --recurse-submodules https://github.com/srmainwaring/gz-python.git
+git clone https://github.com/srmainwaring/gz-python.git
 ```
 
 ### Build with CMake
diff --git a/cmake/dependencies/CMakeLists.txt b/cmake/dependencies/CMakeLists.txt
new file mode 100644
index 0000000..55e7fd3
--- /dev/null
+++ b/cmake/dependencies/CMakeLists.txt
@@ -0,0 +1,42 @@
+include(FetchContent)
+
+#============================================================================
+# Declare all dependencies first
+
+if(BUILD_protobuf)
+  set(protobuf_BUILD_TESTS OFF CACHE INTERNAL "")
+  FetchContent_Declare(
+    protobuf
+    GIT_REPOSITORY "https://github.com/protocolbuffers/protobuf.git"
+    GIT_TAG "v21.5"
+    GIT_SUBMODULES ""
+  )
+endif()
+
+if(BUILD_pybind11_protobuf)
+  FetchContent_Declare(
+    pybind11_protobuf
+    GIT_REPOSITORY "https://github.com/srmainwaring/pybind11_protobuf"
+    GIT_TAG "eea7c8e839bbcd624d8a8230773829cd49e41cba"
+  )
+endif()
+
+#============================================================================
+# Make dependencies avaialble
+
+if(BUILD_protobuf)
+  message(CHECK_START "Fetching protobuf")
+  list(APPEND CMAKE_MESSAGE_INDENT "  ")
+  FetchContent_MakeAvailable(protobuf)
+  list(POP_BACK CMAKE_MESSAGE_INDENT)
+  message(CHECK_PASS "fetched")
+endif()
+
+if(BUILD_pybind11_protobuf)
+  message(CHECK_START "Fetching pybind11_protobuf")
+  list(APPEND CMAKE_MESSAGE_INDENT "  ")
+  FetchContent_MakeAvailable(pybind11_protobuf)
+  list(POP_BACK CMAKE_MESSAGE_INDENT)
+  message(CHECK_PASS "fetched")
+endif()
+
diff --git a/cmake/deps.cmake b/cmake/deps.cmake
new file mode 100644
index 0000000..9569ef9
--- /dev/null
+++ b/cmake/deps.cmake
@@ -0,0 +1,8 @@
+if(NOT BUILD_protobuf)
+  find_package(protobuf REQUIRED)
+endif()
+
+if(NOT BUILD_pybind11_protobuf)
+  find_package(pybind11_protobuf REQUIRED)
+endif()
+
diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
deleted file mode 100644
index e01faf2..0000000
--- a/external/CMakeLists.txt
+++ /dev/null
@@ -1 +0,0 @@
-add_subdirectory(pybind11_protobuf)
diff --git a/external/com_google_protobuf_build.patch b/external/com_google_protobuf_build.patch
deleted file mode 100644
index 8aec915..0000000
--- a/external/com_google_protobuf_build.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- BUILD
-+++ BUILD
-@@ -993,6 +993,7 @@
-         "//conditions:default": [],
-         ":use_fast_cpp_protos": ["//external:python_headers"],
-     }),
-+    visibility = ["//visibility:public"],
- )
-
- config_setting(
diff --git a/external/pybind11_protobuf b/external/pybind11_protobuf
deleted file mode 160000
index ec0d86e..0000000
--- a/external/pybind11_protobuf
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit ec0d86e48b40ba12cb89e67cec2ba64f4b961dee