summaryrefslogtreecommitdiff
path: root/cmake/NodeJS.cmake
blob: 8e0ec56982335af1d7f2e5098469c89121ded049 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# NOTE: We're using a patched version of the original https://github.com/cjntaylor/node-cmake

# Our version is in https://github.com/mapbox/node-cmake/blob/mapbox-gl-native/NodeJS.cmake and
# contains these patches:
# - https://github.com/cjntaylor/node-cmake/pull/20
# - https://github.com/cjntaylor/node-cmake/pull/22
# - https://github.com/cjntaylor/node-cmake/pull/23

# Defaults for standard Node.js builds
set(NODEJS_DEFAULT_URL https://nodejs.org/download/release)
set(NODEJS_DEFAULT_VERSION installed)
set(NODEJS_VERSION_FALLBACK latest)
set(NODEJS_DEFAULT_NAME node)
set(NODEJS_DEFAULT_CHECKSUM SHASUMS256.txt)
set(NODEJS_DEFAULT_CHECKTYPE SHA256)

include(CMakeParseArguments)

# Find a path by walking upward from a base directory until the path is
# found. Sets the variable ${PATH} to False if the path can't
# be determined
function(find_path_parent NAME BASE PATH)
    set(ROOT ${BASE})
    set(${PATH} ${ROOT}/${NAME} PARENT_SCOPE)
    set(DRIVE "^[A-Za-z]?:?/$")
    while(NOT ROOT MATCHES ${DRIVE} AND NOT EXISTS ${ROOT}/${NAME})
        get_filename_component(ROOT ${ROOT} DIRECTORY)
        set(${PATH} ${ROOT}/${NAME} PARENT_SCOPE)
    endwhile()
    if(ROOT MATCHES ${DRIVE})
        set(${PATH} False PARENT_SCOPE)
    endif()
endfunction()

# Shortcut for finding standard node module locations
macro(find_nodejs_module NAME BASE PATH)
    find_path_parent(node_modules/${NAME} ${BASE} ${PATH})
endmacro()

# Download with a bit of nice output (without spewing progress)
function(download_file DESCRIPTION URL FILE)
    message(STATUS "Downloading: ${URL}")
    file(DOWNLOAD
        ${URL}
        ${FILE}.tmp
        ${ARGN}
        STATUS RESULT
    )
    list(GET RESULT 0 STATUS)
    if(STATUS)
        list(GET result 1 MESSAGE)
        message(FATAL_ERROR "Unable to download ${DESCRIPTION} from ${URL}: ${MESSAGE}")
    else()
        file(RENAME ${FILE}.tmp ${FILE})
    endif()
endfunction()

# Embedded win_delay_load_hook file so that this file can be copied
# into projects directly (recommended practice)
function(nodejs_generate_delayload_hook OUTPUT)
    file(WRITE  ${OUTPUT} "")
    file(APPEND ${OUTPUT} "/*\n")
    file(APPEND ${OUTPUT} " * When this file is linked to a DLL, it sets up a delay-load hook that\n")
    file(APPEND ${OUTPUT} " * intervenes when the DLL is trying to load 'node.exe' or 'iojs.exe'\n")
    file(APPEND ${OUTPUT} " * dynamically. Instead of trying to locate the .exe file it'll just return\n")
    file(APPEND ${OUTPUT} " * a handle to the process image.\n")
    file(APPEND ${OUTPUT} " *\n")
    file(APPEND ${OUTPUT} " * This allows compiled addons to work when node.exe or iojs.exe is renamed.\n")
    file(APPEND ${OUTPUT} " */\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "#ifdef _MSC_VER\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "#ifndef DELAYIMP_INSECURE_WRITABLE_HOOKS\n")
    file(APPEND ${OUTPUT} "#define DELAYIMP_INSECURE_WRITABLE_HOOKS\n")
    file(APPEND ${OUTPUT} "#endif\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "#ifndef WIN32_LEAN_AND_MEAN\n")
    file(APPEND ${OUTPUT} "#define WIN32_LEAN_AND_MEAN\n")
    file(APPEND ${OUTPUT} "#endif\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "#include <windows.h>\n")
    file(APPEND ${OUTPUT} "#include <Shlwapi.h>\n")
    file(APPEND ${OUTPUT} "#include <delayimp.h>\n")
    file(APPEND ${OUTPUT} "#include <string.h>\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "static FARPROC WINAPI load_exe_hook(unsigned int event, DelayLoadInfo* info) {\n")
    file(APPEND ${OUTPUT} "  if (event != dliNotePreLoadLibrary) return NULL;\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "  if (_stricmp(info->szDll, \"iojs.exe\") != 0 &&\n")
    file(APPEND ${OUTPUT} "      _stricmp(info->szDll, \"node.exe\") != 0 &&\n")
    file(APPEND ${OUTPUT} "      _stricmp(info->szDll, \"node.dll\") != 0)\n")
    file(APPEND ${OUTPUT} "    return NULL;\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "  // Get a handle to the current process executable.\n")
    file(APPEND ${OUTPUT} "  HMODULE processModule = GetModuleHandle(NULL);\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "  // Get the path to the executable.\n")
    file(APPEND ${OUTPUT} "  TCHAR processPath[_MAX_PATH];\n")
    file(APPEND ${OUTPUT} "  GetModuleFileName(processModule, processPath, _MAX_PATH);\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "  // Get the name of the current executable.\n")
    file(APPEND ${OUTPUT} "  LPSTR processName = PathFindFileName(processPath);\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "  // If the current process is node or iojs, then just return the proccess \n")
    file(APPEND ${OUTPUT} "  // module.\n")
    file(APPEND ${OUTPUT} "  if (_stricmp(processName, \"node.exe\") == 0 ||\n")
    file(APPEND ${OUTPUT} "      _stricmp(processName, \"iojs.exe\") == 0) {\n")
    file(APPEND ${OUTPUT} "    return (FARPROC) processModule;\n")
    file(APPEND ${OUTPUT} "  }\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "  // If it is another process, attempt to load 'node.dll' from the same \n")
    file(APPEND ${OUTPUT} "  // directory.\n")
    file(APPEND ${OUTPUT} "  PathRemoveFileSpec(processPath);\n")
    file(APPEND ${OUTPUT} "  PathAppend(processPath, \"node.dll\");\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "  HMODULE nodeDllModule = GetModuleHandle(processPath);\n")
    file(APPEND ${OUTPUT} "  if(nodeDllModule != NULL) {\n")
    file(APPEND ${OUTPUT} "    // This application has a node.dll in the same directory as the executable,\n")
    file(APPEND ${OUTPUT} "    // use that.\n")
    file(APPEND ${OUTPUT} "    return (FARPROC) nodeDllModule;\n")
    file(APPEND ${OUTPUT} "  }\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "  // Fallback to the current executable, which must statically link to \n")
    file(APPEND ${OUTPUT} "  // node.lib\n")
    file(APPEND ${OUTPUT} "  return (FARPROC) processModule;\n")
    file(APPEND ${OUTPUT} "}\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "PfnDliHook __pfnDliNotifyHook2 = load_exe_hook;\n")
    file(APPEND ${OUTPUT} "\n")
    file(APPEND ${OUTPUT} "#endif\n")
endfunction()

# Sets up a project to build Node.js native modules
# - Downloads required dependencies and unpacks them to the build directory.
#   Internet access is required the first invocation but not after (
#   provided the download is successful)
# - Sets up several variables for building against the downloaded
#   dependencies
# - Guarded to prevent multiple executions, so a single project hierarchy
#   will only call this once
function(nodejs_init)
    # Prevents this function from executing more than once
    if(NODEJS_INIT)
        return()
    endif()

    # Regex patterns used by the init function for component extraction
    set(HEADERS_MATCH "^([A-Fa-f0-9]+)[ \t]+([^-]+)-(headers|v?[0-9.]+)-(headers|v?[0-9.]+)([.]tar[.]gz)$")
    set(LIB32_MATCH "(^[0-9A-Fa-f]+)[\t ]+(win-x86)?(/)?([^/]*)(.lib)$")
    set(LIB64_MATCH "(^[0-9A-Fa-f]+)[\t ]+(win-)?(x64/)(.*)(.lib)$")

    # Parse function arguments
    cmake_parse_arguments(nodejs_init
        "" "URL;NAME;VERSION;CHECKSUM;CHECKTYPE" "" ${ARGN}
    )

    # Allow the download URL to be overridden by command line argument
    # NODEJS_URL
    if(NODEJS_URL)
        set(URL ${NODEJS_URL})
    else()
        # Use the argument if specified, falling back to the default
        set(URL ${NODEJS_DEFAULT_URL})
        if(nodejs_init_URL)
            set(URL ${nodejs_init_URL})
        endif()
    endif()

    # Allow name to be overridden by command line argument NODEJS_NAME
    if(NODEJS_NAME)
        set(NAME ${NODEJS_NAME})
    else()
        # Use the argument if specified, falling back to the default
        set(NAME ${NODEJS_DEFAULT_NAME})
        if(nodejs_init_NAME)
            set(NAME ${nodejs_init_NAME})
        endif()
    endif()

    # Allow the checksum file to be overridden by command line argument
    # NODEJS_CHECKSUM
    if(NODEJS_CHECKSUM)
        set(CHECKSUM ${NODEJS_CHECKSUM})
    else()
        # Use the argument if specified, falling back to the default
        set(CHECKSUM ${NODEJS_DEFAULT_CHECKSUM})
        if(nodejs_init_CHECKSUM)
            set(CHECKSUM ${nodejs_init_CHECKSUM})
        endif()
    endif()

    # Allow the checksum type to be overriden by the command line argument
    # NODEJS_CHECKTYPE
    if(NODEJS_CHECKTYPE)
        set(CHECKTYPE ${NODEJS_CHECKTYPE})
    else()
        # Use the argument if specified, falling back to the default
        set(CHECKTYPE ${NODEJS_DEFAULT_CHECKTYPE})
        if(nodejs_init_CHECKTYPE)
            set(CHECKTYPE ${nodejs_init_CHECKTYPE})
        endif()
    endif()

    # Allow the version to be overridden by the command line argument
    # NODEJS_VERSION
    if(NODEJS_VERSION)
        set(VERSION ${NODEJS_VERSION})
    else()
        # Use the argument if specified, falling back to the default
        set(VERSION ${NODEJS_DEFAULT_VERSION})
        if(nodejs_init_VERSION)
            set(VERSION ${nodejs_init_VERSION})
        endif()
    endif()

    # "installed" is a special version that tries to use the currently
    # installed version (determined by running node)
    set(NODEJS_INSTALLED False CACHE BOOL "Node.js install status" FORCE)
    if(VERSION STREQUAL "installed")
        if(NOT NAME STREQUAL ${NODEJS_DEFAULT_NAME})
            message(FATAL_ERROR
                "'Installed' version identifier can only be used with"
                "the core Node.js library"
            )
        endif()
        # Fall back to the "latest" version if node isn't installed
        set(VERSION ${NODEJS_VERSION_FALLBACK})
        find_program(NODEJS_BINARY NAMES nodejs node)
        if(NODEJS_BINARY)
            execute_process(
                COMMAND ${NODEJS_BINARY} --version
                RESULT_VARIABLE INSTALLED_VERSION_RESULT
                OUTPUT_VARIABLE INSTALLED_VERSION
                OUTPUT_STRIP_TRAILING_WHITESPACE
            )
            if(INSTALLED_VERSION_RESULT STREQUAL "0")
                set(NODEJS_INSTALLED True CACHE BOOL
                    "Node.js install status" FORCE
                )
                set(VERSION ${INSTALLED_VERSION})
            endif()
        endif()
    endif()

    # Create a temporary download directory
    set(TEMP ${CMAKE_CURRENT_BINARY_DIR}/temp)
    file(MAKE_DIRECTORY ${TEMP})

    # Unless the target is special version "latest", the parameters
    # necessary to construct the root path are known
    if(NOT VERSION STREQUAL "latest")
        set(ROOT ${CMAKE_CURRENT_BINARY_DIR}/${NAME}/${VERSION})
        # Extract checksums from the existing checksum file
        set(CHECKSUM_TARGET ${ROOT}/CHECKSUM)
    endif()

    # If we're trying to determine the version or we haven't saved the
    # checksum file for this version, download it from the specified server
    if(VERSION STREQUAL "latest" OR
      (DEFINED ROOT AND NOT EXISTS ${ROOT}/CHECKSUM))
        if(DEFINED ROOT)
            # Clear away the old checksum in case the new one is different
            # and/or it fails to download
            file(REMOVE ${ROOT}/CHECKSUM)
        endif()
        file(REMOVE ${TEMP}/CHECKSUM)
        download_file(
            "checksum file"
            ${URL}/${VERSION}/${CHECKSUM}
            ${TEMP}/CHECKSUM
            INACTIVITY_TIMEOUT 10
        )
        # Extract checksums from the temporary file
        set(CHECKSUM_TARGET ${TEMP}/CHECKSUM)
    endif()

    # Extract the version, name, header archive and archive checksum
    # from the file. This first extract is what defines / specifies the
    # actual version number and name.
    file(STRINGS
        ${CHECKSUM_TARGET} HEADERS_CHECKSUM
        REGEX ${HEADERS_MATCH}
        LIMIT_COUNT 1
    )
    if(NOT HEADERS_CHECKSUM)
        file(REMOVE ${TEMP}/CHECKSUM)
        if(DEFINED ROOT)
            file(REMOVE ${ROOT}/CHECKSUM)
        endif()
        message(FATAL_ERROR "Unable to extract header archive checksum")
    endif()
    string(REGEX MATCH ${HEADERS_MATCH} HEADERS_CHECKSUM ${HEADERS_CHECKSUM})
    set(HEADERS_CHECKSUM ${CMAKE_MATCH_1})
    set(NAME ${CMAKE_MATCH_2})
    if(CMAKE_MATCH_3 STREQUAL "headers")
        set(VERSION ${CMAKE_MATCH_4})
    else()
        set(VERSION ${CMAKE_MATCH_3})
    endif()
    set(HEADERS_ARCHIVE
        ${CMAKE_MATCH_2}-${CMAKE_MATCH_3}-${CMAKE_MATCH_4}${CMAKE_MATCH_5}
    )
    # Make sure that the root directory exists, and that the checksum
    # file has been moved over from temp
    if(DEFINED ROOT)
        set(OLD_ROOT ${ROOT})
    endif()
    set(ROOT ${CMAKE_CURRENT_BINARY_DIR}/${NAME}/${VERSION})
    if(DEFINED OLD_ROOT AND NOT ROOT STREQUAL "${OLD_ROOT}")
        file(REMOVE ${TEMP}/CHECKSUM)
        file(REMOVE ${ROOT}/CHECKSUM)
        message(FATAL_ERROR "Version/Name mismatch")
    endif()
    file(MAKE_DIRECTORY ${ROOT})
    if(EXISTS ${TEMP}/CHECKSUM)
        file(REMOVE ${ROOT}/CHECKSUM)
        file(RENAME ${TEMP}/CHECKSUM ${ROOT}/CHECKSUM)
    endif()

    # Now that its fully resolved, report the name and version of Node.js being
    # used
    message(STATUS "NodeJS: Using ${NAME}, version ${VERSION}")

    # Download the headers for the version being used
    # Theoretically, these could be found by searching the installed
    # system, but in practice, this can be error prone. They're provided
    # on the download servers, so just use the ones there.
    if(NOT EXISTS ${ROOT}/include)
        file(REMOVE ${TEMP}/${HEADERS_ARCHIVE})
        download_file(
            "Node.js headers"
            ${URL}/${VERSION}/${HEADERS_ARCHIVE}
            ${TEMP}/${HEADERS_ARCHIVE}
            INACTIVITY_TIMEOUT 10
            EXPECTED_HASH ${CHECKTYPE}=${HEADERS_CHECKSUM}
        )
        execute_process(
            COMMAND ${CMAKE_COMMAND} -E tar xfz ${TEMP}/${HEADERS_ARCHIVE}
            WORKING_DIRECTORY ${TEMP}
        )

        # This adapts the header extraction to support a number of different
        # header archive contents in addition to the one used by the
        # default Node.js library
        unset(NODEJS_HEADERS_PATH CACHE)
        find_path(NODEJS_HEADERS_PATH
            NAMES src include
            PATHS
                ${TEMP}/${NAME}-${VERSION}-headers
                ${TEMP}/${NAME}-${VERSION}
                ${TEMP}/${NODEJS_DEFAULT_NAME}-${VERSION}-headers
                ${TEMP}/${NODEJS_DEFAULT_NAME}-${VERSION}
                ${TEMP}/${NODEJS_DEFAULT_NAME}
                ${TEMP}
            NO_DEFAULT_PATH
        )
        if(NOT NODEJS_HEADERS_PATH)
            message(FATAL_ERROR "Unable to find extracted headers folder")
        endif()

        # Move the headers into a standard location with a standard layout
        file(REMOVE ${TEMP}/${HEADERS_ARCHIVE})
        file(REMOVE_RECURSE ${ROOT}/include)
        if(EXISTS ${NODEJS_HEADERS_PATH}/include/node)
            file(RENAME ${NODEJS_HEADERS_PATH}/include/node ${ROOT}/include)
        elseif(EXISTS ${NODEJS_HEADERS_PATH}/src)
            file(MAKE_DIRECTORY ${ROOT}/include)
            if(NOT EXISTS ${NODEJS_HEADERS_PATH}/src)
                file(REMOVE_RECURSE ${ROOT}/include)
                message(FATAL_ERROR "Unable to find core headers")
            endif()
            file(COPY ${NODEJS_HEADERS_PATH}/src/
                DESTINATION ${ROOT}/include
            )
            if(NOT EXISTS ${NODEJS_HEADERS_PATH}/deps/uv/include)
                file(REMOVE_RECURSE ${ROOT}/include)
                message(FATAL_ERROR "Unable to find libuv headers")
            endif()
            file(COPY ${NODEJS_HEADERS_PATH}/deps/uv/include/
                DESTINATION ${ROOT}/include
            )
            if(NOT EXISTS ${NODEJS_HEADERS_PATH}/deps/v8/include)
                file(REMOVE_RECURSE ${ROOT}/include)
                message(FATAL_ERROR "Unable to find v8 headers")
            endif()
            file(COPY ${NODEJS_HEADERS_PATH}/deps/v8/include/
                DESTINATION ${ROOT}/include
            )
            if(NOT EXISTS ${NODEJS_HEADERS_PATH}/deps/zlib)
                file(REMOVE_RECURSE ${ROOT}/include)
                message(FATAL_ERROR "Unable to find zlib headers")
            endif()
            file(COPY ${NODEJS_HEADERS_PATH}/deps/zlib/
                DESTINATION ${ROOT}/include
            )
        endif()
        file(REMOVE_RECURSE ${NODEJS_HEADERS_PATH})
        unset(NODEJS_HEADERS_PATH CACHE)
    endif()

    # Only download the libraries on windows, since its the only place
    # its necessary. Note, this requires rerunning CMake if moving
    # a module from one platform to another (should happen automatically
    # with most generators)
    if(WIN32)
        # Download the win32 library for linking
        file(STRINGS
            ${ROOT}/CHECKSUM LIB32_CHECKSUM
            LIMIT_COUNT 1
            REGEX ${LIB32_MATCH}
        )
        if(NOT LIB32_CHECKSUM)
            message(FATAL_ERROR "Unable to extract x86 library checksum")
        endif()
        string(REGEX MATCH ${LIB32_MATCH} LIB32_CHECKSUM ${LIB32_CHECKSUM})
        set(LIB32_CHECKSUM ${CMAKE_MATCH_1})
        set(LIB32_PATH     win-x86)
        set(LIB32_NAME     ${CMAKE_MATCH_4}${CMAKE_MATCH_5})
        set(LIB32_TARGET   ${CMAKE_MATCH_2}${CMAKE_MATCH_3}${LIB32_NAME})
        if(NOT EXISTS ${ROOT}/${LIB32_PATH})
            file(REMOVE_RECURSE ${TEMP}/${LIB32_PATH})
            download_file(
               "Node.js windows library (32-bit)"
               ${URL}/${VERSION}/${LIB32_TARGET}
               ${TEMP}/${LIB32_PATH}/${LIB32_NAME}
               INACTIVITY_TIMEOUT 10
               EXPECTED_HASH ${CHECKTYPE}=${LIB32_CHECKSUM}
            )
            file(REMOVE_RECURSE ${ROOT}/${LIB32_PATH})
            file(MAKE_DIRECTORY ${ROOT}/${LIB32_PATH})
            file(RENAME
                ${TEMP}/${LIB32_PATH}/${LIB32_NAME}
                ${ROOT}/${LIB32_PATH}/${LIB32_NAME}
            )
            file(REMOVE_RECURSE ${TEMP}/${LIB32_PATH})
        endif()

        # Download the win64 library for linking
        file(STRINGS
            ${ROOT}/CHECKSUM LIB64_CHECKSUM
            LIMIT_COUNT 1
            REGEX ${LIB64_MATCH}
        )
        if(NOT LIB64_CHECKSUM)
            message(FATAL_ERROR "Unable to extract x64 library checksum")
        endif()
        string(REGEX MATCH ${LIB64_MATCH} LIB64_CHECKSUM ${LIB64_CHECKSUM})
        set(LIB64_CHECKSUM ${CMAKE_MATCH_1})
        set(LIB64_PATH     win-x64)
        set(LIB64_NAME     ${CMAKE_MATCH_4}${CMAKE_MATCH_5})
        set(LIB64_TARGET   ${CMAKE_MATCH_2}${CMAKE_MATCH_3}${LIB64_NAME})
        if(NOT EXISTS ${ROOT}/${LIB64_PATH})
            file(REMOVE_RECURSE ${TEMP}/${LIB64_PATH})
            download_file(
               "Node.js windows library (64-bit)"
               ${URL}/${VERSION}/${LIB64_TARGET}
               ${TEMP}/${LIB64_PATH}/${LIB64_NAME}
               INACTIVITY_TIMEOUT 10
               EXPECTED_HASH ${CHECKTYPE}=${LIB64_CHECKSUM}
            )
            file(REMOVE_RECURSE ${ROOT}/${LIB64_PATH})
            file(MAKE_DIRECTORY ${ROOT}/${LIB64_PATH})
            file(RENAME
                ${TEMP}/${LIB64_PATH}/${LIB64_NAME}
                ${ROOT}/${LIB64_PATH}/${LIB64_NAME}
            )
            file(REMOVE_RECURSE ${TEMP}/${LIB64_PATH})
        endif()
    endif()

    # The downloaded headers should always be set for inclusion
    list(APPEND INCLUDE_DIRS ${ROOT}/include)

    # Look for the NAN module, and add it to the includes
    find_nodejs_module(
        nan
        ${CMAKE_CURRENT_SOURCE_DIR}
        NODEJS_NAN_DIR
    )
    if(NODEJS_NAN_DIR)
        list(APPEND INCLUDE_DIRS ${NODEJS_NAN_DIR})
    endif()

    # Under windows, we need a bunch of libraries (due to the way
    # dynamic linking works)
    if(WIN32)
        # Generate and use a delay load hook to allow the node binary
        # name to be changed while still loading native modules
        set(DELAY_LOAD_HOOK ${CMAKE_CURRENT_BINARY_DIR}/win_delay_load_hook.c)
        nodejs_generate_delayload_hook(${DELAY_LOAD_HOOK})
        set(SOURCES ${DELAY_LOAD_HOOK})

        # Necessary flags to get delayload working correctly
        list(APPEND LINK_FLAGS
            "-IGNORE:4199"
            "-DELAYLOAD:iojs.exe"
            "-DELAYLOAD:node.exe"
            "-DELAYLOAD:node.dll"
        )

        # Core system libraries used by node
        list(APPEND LIBRARIES
            kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib
            advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib
            odbc32.lib Shlwapi.lib DelayImp.lib
        )

        # Also link to the node stub itself (downloaded above)
        if(CMAKE_CL_64)
            list(APPEND LIBRARIES ${ROOT}/${LIB64_PATH}/${LIB64_NAME})
        else()
            list(APPEND LIBRARIES ${ROOT}/${LIB32_PATH}/${LIB32_NAME})
        endif()
    else()
        # Non-windows platforms should use these flags
        list(APPEND DEFINITIONS _LARGEFILE_SOURCE _FILE_OFFSET_BITS=64)
    endif()

    # Special handling for OSX / clang to allow undefined symbols
    # Define is required by node on OSX
    if(APPLE)
        list(APPEND LINK_FLAGS "-undefined dynamic_lookup")
        list(APPEND DEFINITIONS _DARWIN_USE_64_BIT_INODE=1)
    endif()

    # Export all settings for use as arguments in the rest of the build
    set(NODEJS_VERSION ${VERSION} PARENT_SCOPE)
    set(NODEJS_SOURCES ${SOURCES} PARENT_SCOPE)
    set(NODEJS_INCLUDE_DIRS ${INCLUDE_DIRS} PARENT_SCOPE)
    set(NODEJS_LIBRARIES ${LIBRARIES} PARENT_SCOPE)
    set(NODEJS_LINK_FLAGS ${LINK_FLAGS} PARENT_SCOPE)
    set(NODEJS_DEFINITIONS ${DEFINITIONS} PARENT_SCOPE)

    # Prevents this function from executing more than once
    set(NODEJS_INIT TRUE PARENT_SCOPE)
endfunction()

# Helper function for defining a node module
# After nodejs_init, all of the settings and dependencies necessary to do
# this yourself are defined, but this helps make sure everything is configured
# correctly. Feel free to use it as a model to do this by hand (or to
# tweak this configuration if you need something custom).
function(add_nodejs_module NAME)
    # Make sure node is initialized (variables set) before defining the module
    if(NOT NODEJS_INIT)
        message(FATAL_ERROR
            "Node.js has not been initialized. "
            "Call nodejs_init before adding any modules"
        )
    endif()
    # In order to match node-gyp, we need to build into type specific folders
    # ncmake takes care of this, but be sure to set CMAKE_BUILD_TYPE yourself
    # if invoking CMake directly
    if(NOT CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE)
        message(FATAL_ERROR
            "Configuration type must be specified. "
            "Set CMAKE_BUILD_TYPE or use a different generator"
        )
    endif()

    # A node module is a shared library
    add_library(${NAME} SHARED ${NODEJS_SOURCES} ${ARGN})
    # Add compiler defines for the module
    # Two helpful ones:
    # MODULE_NAME must match the name of the build library, define that here
    target_compile_definitions(${NAME}
        PRIVATE MODULE_NAME=${NAME}
        PUBLIC ${NODEJS_DEFINITIONS}
    )
    # This properly defines includes for the module
    target_include_directories(${NAME} PUBLIC ${NODEJS_INCLUDE_DIRS})

    # Add link flags to the module
    target_link_libraries(${NAME} ${NODEJS_LIBRARIES})

    # Set required properties for the module to build properly
    # Correct naming, symbol visiblity and C++ standard
    set_target_properties(${NAME} PROPERTIES
        OUTPUT_NAME ${NAME}
        PREFIX ""
        SUFFIX ".node"
        MACOSX_RPATH ON
        C_VISIBILITY_PRESET hidden
        CXX_VISIBILITY_PRESET hidden
        POSITION_INDEPENDENT_CODE TRUE
        CMAKE_CXX_STANDARD_REQUIRED TRUE
        CXX_STANDARD 11
        LINK_FLAGS "${NODEJS_LINK_FLAGS}"
    )

    # Make sure we're buiilding in a build specific output directory
    # Only necessary on single-target generators (Make, Ninja)
    # Multi-target generators do this automatically
    # This (luckily) mirrors node-gyp conventions
    if(NOT CMAKE_CONFIGURATION_TYPES)
        set_property(TARGET ${NAME} PROPERTY
            LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BUILD_TYPE}
        )
    endif()
endfunction()