diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 00000000..5cf2e587
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,99 @@
+# 3 june 2016
+cmake_minimum_required(VERSION 2.8.11)
+
+project(libui LANGUAGES C CXX)
+option(BUILD_SHARED_LIBS "Whether to build libui as a shared library or a static library" ON)
+
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/out")
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/out")
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/out")
+set(CMAKE_PDB_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/out")
+
+if(APPLE)
+	set(_OSNAME darwin)
+elseif(WIN32)
+	set(_OSNAME windows)
+else()
+	set(_OSNAME unix)
+endif()
+
+if(BUILD_SHARED_LIBS)
+	# shared libraries link against system libs; executables don't
+	set(_LIBUI_LINKMODE PRIVATE)
+else()
+	# static libraries don't link against system libs; executables do
+	set(_LIBUI_LINKMODE INTERFACE)
+endif()
+
+# common flags
+if(MSVC)
+	# TODO subsystem version
+
+	# TODO /Wall does too much
+	# TODO -Wno-switch equivalent
+	# TODO /sdl turns C4996 into an ERROR
+	# don't use /analyze; that requires us to write annotations everywhere
+	# TODO undecided flags from qo?
+	# /RTCc is not supplied because it's discouraged as of VS2015; see https://www.reddit.com/r/cpp/comments/46mhne/rtcc_rejects_conformant_code_with_visual_c_2015/d06auq5
+	# /EHsc is to shut the compiler up in some cases
+	# TODO make /EHsc C++-only
+	set(_COMMON_CFLAGS
+		/W4 /wd4100
+		/bigobj /nologo
+		/RTC1 /RTCs /RTCu
+		/EHsc
+	)
+
+	# note the /MANIFEST:NO (which must be / and uppercase); thanks FraGag (https://github.com/andlabs/libui/issues/93#issuecomment-223183436)
+	# TODO warnings on undefined symbols
+	set(_COMMON_LDFLAGS
+		/LARGEADDRESSAWARE
+		/NOLOGO
+		/INCREMENTAL:NO
+		/MANIFEST:NO
+	)
+
+	# TODO autogenerate a .def file?
+else()
+endif()
+
+add_subdirectory("common")
+add_subdirectory("${_OSNAME}")
+add_library(${_LIBUINAME} ${_LIBUI_SOURCES})
+target_include_directories(${_LIBUINAME}
+	PUBLIC .
+	PRIVATE ${_LIBUI_INCLUEDIRS})
+target_compile_definitions(${_LIBUINAME}
+	PRIVATE ${_LIBUI_DEFS})
+target_compile_options(${_LIBUINAME}
+	PUBLIC ${_COMMON_CFLAGS}
+	PRIVATE ${_LIBUI_CFLAGS})
+# TODO link directories?
+target_link_libraries(${_LIBUINAME}
+	${_LIBUI_LINKMODE} ${_LIBUI_LIBS})
+# on Windows the linker for static libraries is different; don't give it the flags
+# TODO are these inherited?
+if(BUILD_SHARED_LIBS)
+	set_property(TARGET ${_LIBUINAME} APPEND PROPERTY
+		LINK_FLAGS ${_LIBUI_LDFLAGS})
+endif()
+if(NOT BUILD_SHARED_LIBS)
+	_handle_static()
+	target_compile_definitions(${_LIBUINAME}
+			PRIVATE _UI_STATIC)
+endif()
+# don't put this in the OS CMakeLists.txt to be safe about quoting
+if(WIN32)
+	target_compile_definitions(${_LIBUINAME}
+		PRIVATE "_UI_EXTERN=__declspec(dllexport) extern")
+else()
+	target_compile_definitions(${_LIBUINAME}
+		PRIVATE "_UI_EXTERN=__attribute__((visibility(\"default\"))) extern")
+endif()
+
+macro(_add_exec _name)
+	add_executable(${_name}
+		WIN32 EXCLUDE_FROM_ALL
+		${ARGN})
+	target_link_libraries(${_name} libui)
+endmacro()
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
new file mode 100644
index 00000000..91d79493
--- /dev/null
+++ b/common/CMakeLists.txt
@@ -0,0 +1,16 @@
+# 3 june 2016
+
+list(APPEND _LIBUI_SOURCES
+	common/areaevents.c
+	common/control.c
+	common/debug.c
+	common/matrix.c
+	common/shouldquit.c
+	common/userbugs.c
+)
+set(_LIBUI_SOURCES ${_LIBUI_SOURCES} PARENT_SCOPE)
+
+list(APPEND _LIBUI_INCLUDEDIRS
+	common
+)
+set(_LIBUI_INCLUDEDIRS ${_LIBUI_INCLUDEDIRS} PARENT_SCOPE)
diff --git a/migrate_build/CMakeLists.txt b/migrate_build/CMakeLists.txt
index 8d6aa603..79ebc0a8 100644
--- a/migrate_build/CMakeLists.txt
+++ b/migrate_build/CMakeLists.txt
@@ -34,11 +34,6 @@ if(WIN32)
 	endif()
 endif()
 
-set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/out")
-set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/out")
-set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/out")
-set(CMAKE_PDB_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/out")
-
 # now that we called project(), load our config variables
 macro(cfgcopy _prefix)
 	set(${_prefix}_STATIC "${${_prefix}_DEBUG}")
@@ -180,17 +175,6 @@ else()
 		PRIVATE "_UI_EXTERN=__attribute__((visibility(\"default\"))) extern")
 endif()
 
-macro(_add_exec _name)
-	add_executable(${_name}
-		WIN32 EXCLUDE_FROM_ALL
-		${ARGN}
-		${_RESOURCES_RC})
-	target_link_libraries(${_name} libui)
-	if(NOT _SHARED)
-		target_link_libraries(${_name} ${_PLATFORM_LIBS})
-	endif()
-endmacro()
-
 add_subdirectory("test")
 set_target_properties(tester PROPERTIES
 	OUTPUT_NAME test
diff --git a/migrate_build/build/GNUbasemsvc.mk b/migrate_build/build/GNUbasemsvc.mk
index 5078ac7c..e69de29b 100644
--- a/migrate_build/build/GNUbasemsvc.mk
+++ b/migrate_build/build/GNUbasemsvc.mk
@@ -1,117 +0,0 @@
-# 16 october 2015
-
-# IMPORTANT
-# Do NOT use / for command-line options here!
-# This breaks on GNU makes that come with some versions of
-# MinGW because they mangle things that start with /, thinking that
-# those arguments are Unix paths that need to be converted to
-# Windows paths. This cannot be turned off. -_-'
-# MSDN says cl, rc, and link all accept - instead of /, so we're good.
-# See also:
-# - https://github.com/andlabs/libui/issues/16
-# - http://www.mingw.org/wiki/Posix_path_conversion
-# - http://www.mingw.org/wiki/FAQ
-# - http://stackoverflow.com/questions/7250130/how-to-stop-mingw-and-msys-from-mangling-path-names-given-at-the-command-line
-# - http://stackoverflow.com/questions/28533664/how-to-prevent-msys-to-convert-the-file-path-for-an-external-program
-
-# TODO subsystem version
-
-# TODO silence compiler non-diagnostics (/nologo is not enough)
-
-# Global flags.
-
-# TODO /Wall does too much
-# TODO -Wno-switch equivalent
-# TODO /sdl turns C4996 into an ERROR
-# TODO loads of warnings in the system header files
-# TODO /analyze requires us to write annotations everywhere
-# TODO undecided flags from qo?
-# -RTCc is not supplied because it's discouraged as of VS2015; see https://www.reddit.com/r/cpp/comments/46mhne/rtcc_rejects_conformant_code_with_visual_c_2015/d06auq5
-CFLAGS += \
-	-W4 \
-	-wd4100 \
-	-TC \
-	-bigobj -nologo \
-	-RTC1 -RTCs -RTCu
-
-# TODO prune these
-# -EHsc is to shut the compiler up in some cases
-CXXFLAGS += \
-	-W4 \
-	-wd4100 \
-	-TP \
-	-bigobj -nologo \
-	-RTC1 -RTCs -RTCu \
-	-EHsc
-
-# TODO warnings on undefined symbols
-LDFLAGS += \
-	-largeaddressaware -nologo -incremental:no
-
-ifneq ($(RELEASE),1)
-	CFLAGS += -Zi
-	CXXFLAGS += -Zi
-	LDFLAGS += -debug
-endif
-
-# Build rules.
-
-OFILES = \
-	$(subst /,_,$(CFILES)) \
-	$(subst /,_,$(CXXFILES)) \
-	$(subst /,_,$(MFILES))
-ifeq (,$(STATIC))
-OFILES += \
-	$(subst /,_,$(RCFILES))
-else
-RESFILES = \
-	$(subst /,_,$(RCFILES))
-endif
-
-OFILES := $(OFILES:%=$(OBJDIR)/%.o)
-
-OUT = $(OUTDIR)/$(NAME)$(SUFFIX)
-ifneq (,$(STATIC))
-RESOUT = $(OUTDIR)/$(NAME).res
-endif
-# otherwise keep $(RESOUT) empty
-
-# TODO use $(CC), $(CXX), $(LD), and s$(RC)
-
-$(OUT): $(OFILES) $(RESOUT) | $(OUTDIR)
-ifeq (,$(STATICLIB))
-	@link -out:$(OUT) $(OFILES) $(LDFLAGS)
-else
-	@lib -out:$(OUT) $(OFILES)
-endif
-	@echo ====== Linked $(OUT)
-
-.SECONDEXPANSION:
-
-# TODO can we put /Fd$@.pdb in a variable?
-$(OBJDIR)/%.c.o: $$(subst _,/,%).c $(HFILES) | $(OBJDIR)
-ifeq ($(RELEASE),1)
-	@cl -Fo:$@ -c $< $(CFLAGS)
-else
-	@cl -Fo:$@ -c $< $(CFLAGS) -Fd$@.pdb
-endif
-	@echo ====== Compiled $<
-
-$(OBJDIR)/%.cpp.o: $$(subst _,/,%).cpp $(HFILES) | $(OBJDIR)
-ifeq ($(RELEASE),1)
-	@cl -Fo:$@ -c $< $(CXXFLAGS)
-else
-	@cl -Fo:$@ -c $< $(CXXFLAGS) -Fd$@.pdb
-endif
-	@echo ====== Compiled $<
-
-# note: don't run cvtres directly; the linker does that for us
-$(OBJDIR)/%.rc.o: $$(subst _,/,%).rc $(HFILES) | $(OBJDIR)
-	@rc -nologo -v -fo $@ $(RCFLAGS) $<
-	@echo ====== Compiled $<
-$(RESOUT): $$(RCFILES) $(HFILES) | $(OUTDIR)
-	@rc -nologo -v -fo $@ $(RCFLAGS) $<
-	@echo ====== Compiled $<
-
-$(OBJDIR) $(OUTDIR):
-	@mkdir $@
diff --git a/migrate_build/build/GNUmakefile.libui b/migrate_build/build/GNUmakefile.libui
index 96f11c28..34f2af12 100644
--- a/migrate_build/build/GNUmakefile.libui
+++ b/migrate_build/build/GNUmakefile.libui
@@ -27,23 +27,11 @@ SUFFIX = $(STATICLIBSUFFIX)
 endif
 
 ifeq ($(TOOLCHAIN),gcc)
-	# make every symbol hidden by default except _UI_EXTERN ones
-	# thanks ebassi in irc.gimp.net/#gtk+
-	CFLAGS += \
-		-D_UI_EXTERN='__attribute__((visibility("default"))) extern' \
-		-fvisibility=hidden
-	CXXFLAGS += \
-		-D_UI_EXTERN='__attribute__((visibility("default"))) extern' \
 		-fvisibility=hidden
 	LDFLAGS += \
 		-fvisibility=hidden
 else
-	# make every symbol hidden by default except _UI_EXTERN ones
 	# TODO autogenerate a .def file?
-	CFLAGS += \
-		-D "_UI_EXTERN=__declspec(dllexport) extern"
-	CXXFLAGS += \
-		-D "_UI_EXTERN=__declspec(dllexport) extern"
 endif
 
 ifeq ($(RELEASE),1)
diff --git a/migrate_build/windows/GNUfiles.mk b/migrate_build/windows/GNUfiles.mk
index 0c3b50fa..e69de29b 100644
--- a/migrate_build/windows/GNUfiles.mk
+++ b/migrate_build/windows/GNUfiles.mk
@@ -1,88 +0,0 @@
-# 22 april 2015
-
-CXXFILES += \
-	windows/alloc.cpp \
-	windows/area.cpp \
-	windows/areadraw.cpp \
-	windows/areaevents.cpp \
-	windows/areascroll.cpp \
-	windows/areautil.cpp \
-	windows/box.cpp \
-	windows/button.cpp \
-	windows/checkbox.cpp \
-	windows/colorbutton.cpp \
-	windows/colordialog.cpp \
-	windows/combobox.cpp \
-	windows/container.cpp \
-	windows/control.cpp \
-	windows/d2dscratch.cpp \
-	windows/datetimepicker.cpp \
-	windows/debug.cpp \
-	windows/draw.cpp \
-	windows/drawmatrix.cpp \
-	windows/drawpath.cpp \
-	windows/drawtext.cpp \
-	windows/dwrite.cpp \
-	windows/editablecombo.cpp \
-	windows/entry.cpp \
-	windows/events.cpp \
-	windows/fontbutton.cpp \
-	windows/fontdialog.cpp \
-	windows/graphemes.cpp \
-	windows/group.cpp \
-	windows/init.cpp \
-	windows/label.cpp \
-	windows/main.cpp \
-	windows/menu.cpp \
-	windows/multilineentry.cpp \
-	windows/parent.cpp \
-	windows/progressbar.cpp \
-	windows/radiobuttons.cpp \
-	windows/separator.cpp \
-	windows/sizing.cpp \
-	windows/slider.cpp \
-	windows/spinbox.cpp \
-	windows/stddialogs.cpp \
-	windows/tab.cpp \
-	windows/tabpage.cpp \
-	windows/text.cpp \
-	windows/utf16.cpp \
-	windows/utilwin.cpp \
-	windows/window.cpp \
-	windows/winpublic.cpp \
-	windows/winutil.cpp
-
-HFILES += \
-	windows/_uipriv_migrate.hpp \
-	windows/area.hpp \
-	windows/compilerver.hpp \
-	windows/draw.hpp \
-	windows/resources.hpp \
-	windows/uipriv_windows.hpp \
-	windows/winapi.hpp
-
-RCFILES += \
-	windows/resources.rc
-
-# LONGTERM split into a separate file or put in GNUmakefile.libui somehow?
-
-# flags for the Windows API
-LDFLAGS += $(NATIVE_UI_LDFLAGS)
-
-# flags for building a shared library
-ifeq (,$(STATIC))
-LDFLAGS += \
-	-dll
-endif
-
-# TODO flags for warning on undefined symbols
-
-# no need for a soname
-
-# TODO .def file
-
-ifneq (,$(STATIC))
-CFLAGS += -D_UI_STATIC
-CXXFLAGS += -D_UI_STATIC
-RCFLAGS += -D _UI_STATIC
-endif
diff --git a/migrate_build/windows/GNUosspecific.mk b/migrate_build/windows/GNUosspecific.mk
index b074e06a..e69de29b 100644
--- a/migrate_build/windows/GNUosspecific.mk
+++ b/migrate_build/windows/GNUosspecific.mk
@@ -1,14 +0,0 @@
-# 16 october 2015
-
-EXESUFFIX = .exe
-LIBSUFFIX = .dll
-OSHSUFFIX = .h
-STATICLIBSUFFIX = .lib
-TOOLCHAIN = msvc
-
-USESSONAME = 0
-
-# notice that usp10.lib comes before gdi32.lib
-# TODO prune this list
-NATIVE_UI_LDFLAGS = \
-	user32.lib kernel32.lib usp10.lib gdi32.lib comctl32.lib uxtheme.lib msimg32.lib comdlg32.lib d2d1.lib dwrite.lib ole32.lib oleaut32.lib oleacc.lib uuid.lib
diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt
new file mode 100644
index 00000000..84d1f2aa
--- /dev/null
+++ b/windows/CMakeLists.txt
@@ -0,0 +1,80 @@
+# 3 june 2016
+
+list(APPEND _LIBUI_SOURCES
+	windows/alloc.cpp
+	windows/area.cpp
+	windows/areadraw.cpp
+	windows/areaevents.cpp
+	windows/areascroll.cpp
+	windows/areautil.cpp
+	windows/box.cpp
+	windows/button.cpp
+	windows/checkbox.cpp
+	windows/colorbutton.cpp
+	windows/colordialog.cpp
+	windows/combobox.cpp
+	windows/container.cpp
+	windows/control.cpp
+	windows/d2dscratch.cpp
+	windows/datetimepicker.cpp
+	windows/debug.cpp
+	windows/draw.cpp
+	windows/drawmatrix.cpp
+	windows/drawpath.cpp
+	windows/drawtext.cpp
+	windows/dwrite.cpp
+	windows/editablecombo.cpp
+	windows/entry.cpp
+	windows/events.cpp
+	windows/fontbutton.cpp
+	windows/fontdialog.cpp
+	windows/graphemes.cpp
+	windows/group.cpp
+	windows/init.cpp
+	windows/label.cpp
+	windows/main.cpp
+	windows/menu.cpp
+	windows/multilineentry.cpp
+	windows/parent.cpp
+	windows/progressbar.cpp
+	windows/radiobuttons.cpp
+	windows/separator.cpp
+	windows/sizing.cpp
+	windows/slider.cpp
+	windows/spinbox.cpp
+	windows/stddialogs.cpp
+	windows/tab.cpp
+	windows/tabpage.cpp
+	windows/text.cpp
+	windows/utf16.cpp
+	windows/utilwin.cpp
+	windows/window.cpp
+	windows/winpublic.cpp
+	windows/winutil.cpp
+	windows/resources.rc
+)
+set(_LIBUI_SOURCES ${_LIBUI_SOURCES} PARENT_SCOPE)
+
+list(APPEND _LIBUI_INCLUDEDIRS
+	windows
+)
+set(_LIBUI_INCLUDEDIRS _LIBUI_INCLUDEDIRS PARENT_SCOPE)
+
+# no special handling of static libraries needed
+set(_LIBUINAME libui PARENT_SCOPE)
+macro(_handle_static)
+endmacro()
+
+# notice that usp10 comes before gdi32
+# TODO prune this list
+set(_LIBUI_LIBS
+	user32 kernel32 usp10 gdi32 comctl32 uxtheme msimg32 comdlg32 d2d1 dwrite ole32 oleaut32 oleacc uuid
+PARENT_SCOPE)
+
+if(NOT MSVC)
+	if(BUILD_SHARED_LIBS)
+		message(FATAL_ERROR
+			"Sorry, but libui for Windows can currently only be built as a static library with MinGW. You will need to either build as a static library or switch to MSVC."
+		)
+	endif()
+endif()