diff --git a/.gitignore b/.gitignore
index 1f3532b366ec6d0b1c5affce0bb9d9efddc1b93b..53671e8b981ae91358073e10cecc4faf6f66d63b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,9 +99,6 @@ tools/.vscode/
 *_wrap.h
 *_loader.c
 
-# Windows
-*.exe
-
 # Mac
 .DS_Store
 build-macos*
@@ -112,8 +109,9 @@ build-macos*
 /extras/tools/libtool-*.tar.xz
 
 # Windows native build
-/contrib/build/
-/contrib/msvc/
+x64/
+Release/
+Debug/
 
 # iOS
 /build-ios*
@@ -144,4 +142,4 @@ compile_commands.json
 *.cmake
 CMakeCache.txt
 Testing
-CMakeFiles
\ No newline at end of file
+CMakeFiles
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..b3ebfe270847252496f2f631a30d2c4a50695c6d
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "extras/scripts/pywinmake"]
+	path = extras/scripts/pywinmake
+	url = https://review.jami.net/pywinmake
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2dd71d125ea42afd9366902370e3141276696668..e62d5ee510edbd3b77f6aed4477e4f8e964dc134 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -304,7 +304,7 @@ if(MSVC)
    ################################################################################
    if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64")
        set_target_properties(${PROJECT_NAME} PROPERTIES
-           TARGET_NAME_RELEASELIB_WIN32 "jami"
+           TARGET_NAME_RELEASE "jami"
        )
    endif()
    ################################################################################
@@ -312,12 +312,12 @@ if(MSVC)
    ################################################################################
    if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64")
        set_target_properties(${PROJECT_NAME} PROPERTIES
-           OUTPUT_DIRECTORY_RELEASELIB_WIN32 "${CMAKE_CURRENT_SOURCE_DIR}/build/${CMAKE_VS_PLATFORM_NAME}/$<CONFIG>/bin/"
+           OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}/build/lib"
        )
    endif()
    if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64")
        set_target_properties(${PROJECT_NAME} PROPERTIES
-           INTERPROCEDURAL_OPTIMIZATION_RELEASELIB_WIN32 "FALSE"
+           INTERPROCEDURAL_OPTIMIZATION_RELEASE "FALSE"
        )
    endif()
    ################################################################################
diff --git a/compat/msvc/package.json b/compat/msvc/package.json
index 72d09ff464d9da3da1c56ea9ba5f54f5e35d72e5..bdf24f736c26a381aa5b58814d04409e82b02753 100644
--- a/compat/msvc/package.json
+++ b/compat/msvc/package.json
@@ -1,21 +1,4 @@
 {
     "name": "daemon",
-    "deps": [
-        "asio",
-        "ffmpeg",
-        "natpmp",
-        "opendht",
-        "pjproject",
-        "portaudio",
-        "secp256k1",
-        "speexdsp",
-        "upnp",
-        "yaml-cpp",
-        "libarchive",
-        "webrtc-audio-processing",
-        "libgit2",
-        "dhtnet"
-    ],
-    "configuration": "ReleaseLib_win32",
     "use_cmake": true
 }
diff --git a/contrib/.gitignore b/contrib/.gitignore
index bcbe1829497e2b5ac357b21ef60fd22fff1773ca..dc89154be72ffe1c25202d9774df3acf0b4a2ea9 100644
--- a/contrib/.gitignore
+++ b/contrib/.gitignore
@@ -6,3 +6,5 @@ arm*
 aarch64*
 i686*
 apple*
+build*
+msvc*
\ No newline at end of file
diff --git a/extras/scripts/pywinmake b/extras/scripts/pywinmake
new file mode 160000
index 0000000000000000000000000000000000000000..512c3691f0b839fd8d8d3766a4d694ff2773642a
--- /dev/null
+++ b/extras/scripts/pywinmake
@@ -0,0 +1 @@
+Subproject commit 512c3691f0b839fd8d8d3766a4d694ff2773642a
diff --git a/extras/scripts/winmake.py b/extras/scripts/winmake.py
new file mode 100644
index 0000000000000000000000000000000000000000..3170b8ddbedb2d7cd85ecb304b641ceae1c4b7e1
--- /dev/null
+++ b/extras/scripts/winmake.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+SPDX-License-Identifier: GPL-3.0-or-later
+Copyright (c) 2023 Savoir-faire Linux
+
+Uses pywinmake to build the daemon and its dependencies.
+"""
+
+import os
+import time
+from datetime import timedelta
+import argparse
+
+from pywinmake.utils import log, logger, sh_exec
+from pywinmake.package import Versioner, Paths, Operation, Package
+from pywinmake.package import get_default_parsed_args
+from pywinmake.builders import MetaBuilder
+
+
+def seconds_to_str(elapsed=None):
+    return str(timedelta(seconds=elapsed))
+
+def build_contrib(args, paths):
+    versioner = Versioner(base_dir=paths.contrib_dir)
+
+    # Exclude packages that are not needed for the daemon.
+    # TODO: libjami: move these to their own contribs and remove the package.json files
+    versioner.exclusion_list = [
+        "liburcu",
+        "lttng-ust",
+        "minizip",
+        "onnx",
+        "opencv",
+        "opencv_contrib",
+    ]
+    versioner.extra_output_dirs = ["msvc"]
+
+    def vs_env_init_cb():
+        # TODO: libjami: replace DAEMON_DIR in ffnvcodec with something else
+        # TODO: libjami: CONTRIB_SRC_DIR is used by ffmpeg
+        # NOTE: MSYS2_BIN defined in build_ffmpeg.bat (might want to remove it)
+        # NOTE: paths.base_dir should be the daemon dir if initialized correctly
+        sh_exec.append_extra_env_vars(
+            {
+                "DAEMON_DIR": paths.base_dir,
+                "CONTRIB_SRC_DIR": os.path.join(paths.contrib_dir, "src"),
+            }
+        )
+        # Find JOM if it is installed. (default C:/Qt/Tools/QtCreator/bin/jom)
+        # Used to accelerate the build process when normally using nmake.
+        qt_tools_dir = os.path.join(os.getenv("QTDIR", "C:\Qt"), "Tools")
+        jom_path = os.path.join(qt_tools_dir, "QtCreator", "bin", "jom", "jom.exe")
+        if os.path.exists(jom_path):
+            log.info("Found JOM at " + jom_path)
+            sh_exec.append_extra_env_vars({"MAKE_TOOL": jom_path})
+
+    versioner.builder.set_vs_env_init_cb(vs_env_init_cb)
+
+    op = Operation.from_string(args.subcommand)
+    log.info(f"op={str(op)}, pkgs={args.pkg}, force={str(args.force)}")
+
+    if op == Operation.CLEAN:
+        versioner.clean_all() if args.pkg == "all" else versioner.clean_pkg(args.pkg)
+    elif args.pkg == "all":
+        versioner.exec_for_all(op=op, force=args.force)
+    else:
+        versioner.exec_for_pkg(args.pkg, op=op, force=args.force, recurse=args.recurse)
+
+def build_from_dir(path, out_dir=None):
+    """Pretty much just for building libjami."""
+    # Make sure our paths are absolute.
+    path = os.path.abspath(path)
+    out_dir = os.path.abspath(out_dir)
+    # Initialize the builder.
+    builder = MetaBuilder(base_dir=path)
+    # Build the package at the given path.
+    out_dir = os.path.join(path, "build") if out_dir is None else out_dir
+    pkg = Package(src_dir=path, build_dir=out_dir)
+    builder.build(pkg)
+
+def main():
+    start_time = time.time()
+
+    parser = argparse.ArgumentParser(
+        description="Build the daemon and its dependencies."
+    )
+    parser.add_argument(
+        "--base-dir",
+        default=None,
+        help="A directory containing a package or the contrib dir.",
+    )
+    parser.add_argument(
+        "--out-dir",
+        default=".",
+        help="Output the build directory for the single package.",
+    )
+    args = get_default_parsed_args(parser)
+    base_dir = args.base_dir
+
+    logger.init(args.log_level, args.verbose, args.indent)
+    sh_exec.set_quiet_mode(args.quiet)
+    sh_exec.set_debug_cmd(args.verbosity == 2)
+
+    build_from_dir_only = False
+    try:
+        # This will search for the base directory containing the contrib directory.
+        paths = Paths(base_dir=base_dir, root_names=["daemon", "jami-daemon"])
+        build_contrib(args, paths)
+    except RuntimeError as e:
+        build_from_dir_only = True
+
+    if build_from_dir_only:
+        build_from_dir(base_dir, args.out_dir)
+
+    log.info("--- %s ---" % seconds_to_str(time.time() - start_time))
+
+    # TODO: implement sha512 hash checking
+    # TODO: implement define-list accumulation
+    # TODO: clarify build vs build_src (also change dir to "native")
+
+
+if __name__ == "__main__":
+    main()