1# CMakeRC is a third-party utility.
2# https://github.com/vector-of-bool/cmrc
3#
4# MIT License
5#
6# Copyright (c) 2017 vector-of-bool <vectorofbool@gmail.com>
7#
8# Permission is hereby granted, free of charge, to any person obtaining a copy
9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in all
16# copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24# SOFTWARE.
25
26
27# This block is executed when generating an intermediate resource file, not when
28# running in CMake configure mode
29if(_CMRC_GENERATE_MODE)
30    # Read in the digits
31    file(READ "${INPUT_FILE}" bytes HEX)
32    # Format each pair into a character literal. Heuristics seem to favor doing
33    # the conversion in groups of five for fastest conversion
34    string(REGEX REPLACE "(..)(..)(..)(..)(..)" "'\\\\x\\1','\\\\x\\2','\\\\x\\3','\\\\x\\4','\\\\x\\5'," chars "${bytes}")
35    # Since we did this in groups, we have some leftovers to clean up
36    string(LENGTH "${bytes}" n_bytes2)
37    math(EXPR n_bytes "${n_bytes2} / 2")
38    math(EXPR remainder "${n_bytes} % 5") # <-- '5' is the grouping count from above
39    set(cleanup_re "$")
40    set(cleanup_sub )
41    while(remainder)
42        set(cleanup_re "(..)${cleanup_re}")
43        set(cleanup_sub "'\\\\x\\${remainder}',${cleanup_sub}")
44        math(EXPR remainder "${remainder} - 1")
45    endwhile()
46    if(NOT cleanup_re STREQUAL "$")
47        string(REGEX REPLACE "${cleanup_re}" "${cleanup_sub}" chars "${chars}")
48    endif()
49    string(CONFIGURE [[
50        namespace { const char file_array[] = { @chars@ 0 }; }
51        namespace cmrc { namespace @NAMESPACE@ { namespace res_chars {
52        extern const char* const @SYMBOL@_begin = file_array;
53        extern const char* const @SYMBOL@_end = file_array + @n_bytes@;
54        }}}
55    ]] code)
56    file(WRITE "${OUTPUT_FILE}" "${code}")
57    # Exit from the script. Nothing else needs to be processed
58    return()
59endif()
60
61set(_version 2.0.0)
62
63cmake_minimum_required(VERSION 3.3)
64include(CMakeParseArguments)
65
66if(COMMAND cmrc_add_resource_library)
67    if(NOT DEFINED _CMRC_VERSION OR NOT (_version STREQUAL _CMRC_VERSION))
68        message(WARNING "More than one CMakeRC version has been included in this project.")
69    endif()
70    # CMakeRC has already been included! Don't do anything
71    return()
72endif()
73
74set(_CMRC_VERSION "${_version}" CACHE INTERNAL "CMakeRC version. Used for checking for conflicts")
75
76set(_CMRC_SCRIPT "${CMAKE_CURRENT_LIST_FILE}" CACHE INTERNAL "Path to CMakeRC script")
77
78function(_cmrc_normalize_path var)
79    set(path "${${var}}")
80    file(TO_CMAKE_PATH "${path}" path)
81    while(path MATCHES "//")
82        string(REPLACE "//" "/" path "${path}")
83    endwhile()
84    string(REGEX REPLACE "/+$" "" path "${path}")
85    set("${var}" "${path}" PARENT_SCOPE)
86endfunction()
87
88get_filename_component(_inc_dir "${CMAKE_BINARY_DIR}/_cmrc/include" ABSOLUTE)
89set(CMRC_INCLUDE_DIR "${_inc_dir}" CACHE INTERNAL "Directory for CMakeRC include files")
90# Let's generate the primary include file
91file(MAKE_DIRECTORY "${CMRC_INCLUDE_DIR}/cmrc")
92set(hpp_content [==[
93#ifndef CMRC_CMRC_HPP_INCLUDED
94#define CMRC_CMRC_HPP_INCLUDED
95
96#include <cassert>
97#include <functional>
98#include <iterator>
99#include <list>
100#include <map>
101#include <mutex>
102#include <string>
103#include <system_error>
104#include <type_traits>
105
106namespace cmrc { namespace detail { struct dummy; } }
107
108#define CMRC_DECLARE(libid) \
109    namespace cmrc { namespace detail { \
110    struct dummy; \
111    static_assert(std::is_same<dummy, ::cmrc::detail::dummy>::value, "CMRC_DECLARE() must only appear at the global namespace"); \
112    } } \
113    namespace cmrc { namespace libid { \
114    cmrc::embedded_filesystem get_filesystem(); \
115    } } static_assert(true, "")
116
117namespace cmrc {
118
119class file {
120    const char* _begin = nullptr;
121    const char* _end = nullptr;
122
123public:
124    using iterator = const char*;
125    using const_iterator = iterator;
126    iterator begin() const noexcept { return _begin; }
127    iterator cbegin() const noexcept { return _begin; }
128    iterator end() const noexcept { return _end; }
129    iterator cend() const noexcept { return _end; }
130    std::size_t size() const { return std::distance(begin(), end()); }
131
132    file() = default;
133    file(iterator beg, iterator end) noexcept : _begin(beg), _end(end) {}
134};
135
136class directory_entry;
137
138namespace detail {
139
140class directory;
141class file_data;
142
143class file_or_directory {
144    union _data_t {
145        class file_data* file_data;
146        class directory* directory;
147    } _data;
148    bool _is_file = true;
149
150public:
151    explicit file_or_directory(file_data& f) {
152        _data.file_data = &f;
153    }
154    explicit file_or_directory(directory& d) {
155        _data.directory = &d;
156        _is_file = false;
157    }
158    bool is_file() const noexcept {
159        return _is_file;
160    }
161    bool is_directory() const noexcept {
162        return !is_file();
163    }
164    const directory& as_directory() const noexcept {
165        assert(!is_file());
166        return *_data.directory;
167    }
168    const file_data& as_file() const noexcept {
169        assert(is_file());
170        return *_data.file_data;
171    }
172};
173
174class file_data {
175public:
176    const char* begin_ptr;
177    const char* end_ptr;
178    file_data(const file_data&) = delete;
179    file_data(const char* b, const char* e) : begin_ptr(b), end_ptr(e) {}
180};
181
182inline std::pair<std::string, std::string> split_path(const std::string& path) {
183    auto first_sep = path.find("/");
184    if (first_sep == path.npos) {
185        return std::make_pair(path, "");
186    } else {
187        return std::make_pair(path.substr(0, first_sep), path.substr(first_sep + 1));
188    }
189}
190
191struct created_subdirectory {
192    class directory& directory;
193    class file_or_directory& index_entry;
194};
195
196class directory {
197    std::list<file_data> _files;
198    std::list<directory> _dirs;
199    std::map<std::string, file_or_directory> _index;
200
201    using base_iterator = std::map<std::string, file_or_directory>::const_iterator;
202
203public:
204
205    directory() = default;
206    directory(const directory&) = delete;
207
208    created_subdirectory add_subdir(std::string name) & {
209        _dirs.emplace_back();
210        auto& back = _dirs.back();
211        auto& fod = _index.emplace(name, file_or_directory{back}).first->second;
212        return created_subdirectory{back, fod};
213    }
214
215    file_or_directory* add_file(std::string name, const char* begin, const char* end) & {
216        assert(_index.find(name) == _index.end());
217        _files.emplace_back(begin, end);
218        return &_index.emplace(name, file_or_directory{_files.back()}).first->second;
219    }
220
221    const file_or_directory* get(const std::string& path) const {
222        auto pair = split_path(path);
223        auto child = _index.find(pair.first);
224        if (child == _index.end()) {
225            return nullptr;
226        }
227        auto& entry  = child->second;
228        if (pair.second.empty()) {
229            // We're at the end of the path
230            return &entry;
231        }
232
233        if (entry.is_file()) {
234            // We can't traverse into a file. Stop.
235            return nullptr;
236        }
237        // Keep going down
238        return entry.as_directory().get(pair.second);
239    }
240
241    class iterator {
242        base_iterator _base_iter;
243        base_iterator _end_iter;
244    public:
245        using value_type = directory_entry;
246        using difference_type = std::ptrdiff_t;
247        using pointer = const value_type*;
248        using reference = const value_type&;
249        using iterator_category = std::input_iterator_tag;
250
251        iterator() = default;
252        explicit iterator(base_iterator iter, base_iterator end) : _base_iter(iter), _end_iter(end) {}
253
254        iterator begin() const noexcept {
255            return *this;
256        }
257
258        iterator end() const noexcept {
259            return iterator(_end_iter, _end_iter);
260        }
261
262        inline value_type operator*() const noexcept;
263
264        bool operator==(const iterator& rhs) const noexcept {
265            return _base_iter == rhs._base_iter;
266        }
267
268        bool operator!=(const iterator& rhs) const noexcept {
269            return !(*this == rhs);
270        }
271
272        iterator operator++() noexcept {
273            auto cp = *this;
274            ++_base_iter;
275            return cp;
276        }
277
278        iterator& operator++(int) noexcept {
279            ++_base_iter;
280            return *this;
281        }
282    };
283
284    using const_iterator = iterator;
285
286    iterator begin() const noexcept {
287        return iterator(_index.begin(), _index.end());
288    }
289
290    iterator end() const noexcept {
291        return iterator();
292    }
293};
294
295inline std::string normalize_path(std::string path) {
296    while (path.find("/") == 0) {
297        path.erase(path.begin());
298    }
299    while (!path.empty() && (path.rfind("/") == path.size() - 1)) {
300        path.pop_back();
301    }
302    auto off = path.npos;
303    while ((off = path.find("//")) != path.npos) {
304        path.erase(path.begin() + off);
305    }
306    return path;
307}
308
309using index_type = std::map<std::string, const cmrc::detail::file_or_directory*>;
310
311} // detail
312
313class directory_entry {
314    std::string _fname;
315    const detail::file_or_directory* _item;
316
317public:
318    directory_entry() = delete;
319    explicit directory_entry(std::string filename, const detail::file_or_directory& item)
320        : _fname(filename)
321        , _item(&item)
322    {}
323
324    const std::string& filename() const & {
325        return _fname;
326    }
327    std::string filename() const && {
328        return std::move(_fname);
329    }
330
331    bool is_file() const {
332        return _item->is_file();
333    }
334
335    bool is_directory() const {
336        return _item->is_directory();
337    }
338};
339
340directory_entry detail::directory::iterator::operator*() const noexcept {
341    assert(begin() != end());
342    return directory_entry(_base_iter->first, _base_iter->second);
343}
344
345using directory_iterator = detail::directory::iterator;
346
347class embedded_filesystem {
348    // Never-null:
349    const cmrc::detail::index_type* _index;
350    const detail::file_or_directory* _get(std::string path) const {
351        path = detail::normalize_path(path);
352        auto found = _index->find(path);
353        if (found == _index->end()) {
354            return nullptr;
355        } else {
356            return found->second;
357        }
358    }
359
360public:
361    explicit embedded_filesystem(const detail::index_type& index)
362        : _index(&index)
363    {}
364
365    file open(const std::string& path) const {
366        auto entry_ptr = _get(path);
367        if (!entry_ptr || !entry_ptr->is_file()) {
368            throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path);
369        }
370        auto& dat = entry_ptr->as_file();
371        return file{dat.begin_ptr, dat.end_ptr};
372    }
373
374    bool is_file(const std::string& path) const noexcept {
375        auto entry_ptr = _get(path);
376        return entry_ptr && entry_ptr->is_file();
377    }
378
379    bool is_directory(const std::string& path) const noexcept {
380        auto entry_ptr = _get(path);
381        return entry_ptr && entry_ptr->is_directory();
382    }
383
384    bool exists(const std::string& path) const noexcept {
385        return !!_get(path);
386    }
387
388    directory_iterator iterate_directory(const std::string& path) const {
389        auto entry_ptr = _get(path);
390        if (!entry_ptr) {
391            throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path);
392        }
393        if (!entry_ptr->is_directory()) {
394            throw std::system_error(make_error_code(std::errc::not_a_directory), path);
395        }
396        return entry_ptr->as_directory().begin();
397    }
398};
399
400}
401
402#endif // CMRC_CMRC_HPP_INCLUDED
403]==])
404
405set(cmrc_hpp "${CMRC_INCLUDE_DIR}/cmrc/cmrc.hpp" CACHE INTERNAL "")
406set(_generate 1)
407if(EXISTS "${cmrc_hpp}")
408    file(READ "${cmrc_hpp}" _current)
409    if(_current STREQUAL hpp_content)
410        set(_generate 0)
411    endif()
412endif()
413file(GENERATE OUTPUT "${cmrc_hpp}" CONTENT "${hpp_content}" CONDITION ${_generate})
414
415add_library(cmrc-base INTERFACE)
416target_include_directories(cmrc-base INTERFACE "${CMRC_INCLUDE_DIR}")
417# Signal a basic C++11 feature to require C++11.
418target_compile_features(cmrc-base INTERFACE cxx_nullptr)
419set_property(TARGET cmrc-base PROPERTY INTERFACE_CXX_EXTENSIONS OFF)
420add_library(cmrc::base ALIAS cmrc-base)
421
422function(cmrc_add_resource_library name)
423    set(args ALIAS NAMESPACE)
424    cmake_parse_arguments(ARG "" "${args}" "" "${ARGN}")
425    # Generate the identifier for the resource library's namespace
426    set(ns_re "[a-zA-Z_][a-zA-Z0-9_]*")
427    if(NOT DEFINED ARG_NAMESPACE)
428        # Check that the library name is also a valid namespace
429        if(NOT name MATCHES "${ns_re}")
430            message(SEND_ERROR "Library name is not a valid namespace. Specify the NAMESPACE argument")
431        endif()
432        set(ARG_NAMESPACE "${name}")
433    else()
434        if(NOT ARG_NAMESPACE MATCHES "${ns_re}")
435            message(SEND_ERROR "NAMESPACE for ${name} is not a valid C++ namespace identifier (${ARG_NAMESPACE})")
436        endif()
437    endif()
438    set(libname "${name}")
439    # Generate a library with the compiled in character arrays.
440    string(CONFIGURE [=[
441        #include <cmrc/cmrc.hpp>
442        #include <map>
443        #include <utility>
444
445        namespace cmrc {
446        namespace @ARG_NAMESPACE@ {
447
448        namespace res_chars {
449        // These are the files which are available in this resource library
450        $<JOIN:$<TARGET_PROPERTY:@libname@,CMRC_EXTERN_DECLS>,
451        >
452        }
453
454        namespace {
455
456        const cmrc::detail::index_type&
457        get_root_index() {
458            static cmrc::detail::directory root_directory_;
459            static cmrc::detail::file_or_directory root_directory_fod{root_directory_};
460            static cmrc::detail::index_type root_index;
461            root_index.emplace("", &root_directory_fod);
462            struct dir_inl {
463                class cmrc::detail::directory& directory;
464            };
465            dir_inl root_directory_dir{root_directory_};
466            (void)root_directory_dir;
467            $<JOIN:$<TARGET_PROPERTY:@libname@,CMRC_MAKE_DIRS>,
468            >
469            $<JOIN:$<TARGET_PROPERTY:@libname@,CMRC_MAKE_FILES>,
470            >
471            return root_index;
472        }
473
474        }
475
476        cmrc::embedded_filesystem get_filesystem() {
477            static auto& index = get_root_index();
478            return cmrc::embedded_filesystem{index};
479        }
480
481        } // @ARG_NAMESPACE@
482        } // cmrc
483    ]=] cpp_content @ONLY)
484    get_filename_component(libdir "${CMAKE_CURRENT_BINARY_DIR}/__cmrc_${name}" ABSOLUTE)
485    get_filename_component(lib_tmp_cpp "${libdir}/lib_.cpp" ABSOLUTE)
486    string(REPLACE "\n        " "\n" cpp_content "${cpp_content}")
487    file(GENERATE OUTPUT "${lib_tmp_cpp}" CONTENT "${cpp_content}")
488    get_filename_component(libcpp "${libdir}/lib.cpp" ABSOLUTE)
489    add_custom_command(OUTPUT "${libcpp}"
490        DEPENDS "${lib_tmp_cpp}" "${cmrc_hpp}"
491        COMMAND ${CMAKE_COMMAND} -E copy_if_different "${lib_tmp_cpp}" "${libcpp}"
492        COMMENT "Generating ${name} resource loader"
493        )
494    # Generate the actual static library. Each source file is just a single file
495    # with a character array compiled in containing the contents of the
496    # corresponding resource file.
497    add_library(${name} STATIC ${libcpp})
498    set_property(TARGET ${name} PROPERTY CMRC_LIBDIR "${libdir}")
499    set_property(TARGET ${name} PROPERTY CMRC_NAMESPACE "${ARG_NAMESPACE}")
500    target_link_libraries(${name} PUBLIC cmrc::base)
501    set_property(TARGET ${name} PROPERTY CMRC_IS_RESOURCE_LIBRARY TRUE)
502    if(ARG_ALIAS)
503        add_library("${ARG_ALIAS}" ALIAS ${name})
504    endif()
505    cmrc_add_resources(${name} ${ARG_UNPARSED_ARGUMENTS})
506endfunction()
507
508function(_cmrc_register_dirs name dirpath)
509    if(dirpath STREQUAL "")
510        return()
511    endif()
512    # Skip this dir if we have already registered it
513    get_target_property(registered "${name}" _CMRC_REGISTERED_DIRS)
514    if(dirpath IN_LIST registered)
515        return()
516    endif()
517    # Register the parent directory first
518    get_filename_component(parent "${dirpath}" DIRECTORY)
519    if(NOT parent STREQUAL "")
520        _cmrc_register_dirs("${name}" "${parent}")
521    endif()
522    # Now generate the registration
523    set_property(TARGET "${name}" APPEND PROPERTY _CMRC_REGISTERED_DIRS "${dirpath}")
524    _cm_encode_fpath(sym "${dirpath}")
525    if(parent STREQUAL "")
526        set(parent_sym root_directory)
527    else()
528        _cm_encode_fpath(parent_sym "${parent}")
529    endif()
530    get_filename_component(leaf "${dirpath}" NAME)
531    set_property(
532        TARGET "${name}"
533        APPEND PROPERTY CMRC_MAKE_DIRS
534        "static auto ${sym}_dir = ${parent_sym}_dir.directory.add_subdir(\"${leaf}\")\;"
535        "root_index.emplace(\"${dirpath}\", &${sym}_dir.index_entry)\;"
536        )
537endfunction()
538
539function(cmrc_add_resources name)
540    get_target_property(is_reslib ${name} CMRC_IS_RESOURCE_LIBRARY)
541    if(NOT TARGET ${name} OR NOT is_reslib)
542        message(SEND_ERROR "cmrc_add_resources called on target '${name}' which is not an existing resource library")
543        return()
544    endif()
545
546    set(options)
547    set(args WHENCE PREFIX)
548    set(list_args)
549    cmake_parse_arguments(ARG "${options}" "${args}" "${list_args}" "${ARGN}")
550
551    if(NOT ARG_WHENCE)
552        set(ARG_WHENCE ${CMAKE_CURRENT_SOURCE_DIR})
553    endif()
554    _cmrc_normalize_path(ARG_WHENCE)
555    get_filename_component(ARG_WHENCE "${ARG_WHENCE}" ABSOLUTE)
556
557    # Generate the identifier for the resource library's namespace
558    get_target_property(lib_ns "${name}" CMRC_NAMESPACE)
559
560    get_target_property(libdir ${name} CMRC_LIBDIR)
561    get_target_property(target_dir ${name} SOURCE_DIR)
562    file(RELATIVE_PATH reldir "${target_dir}" "${CMAKE_CURRENT_SOURCE_DIR}")
563    if(reldir MATCHES "^\\.\\.")
564        message(SEND_ERROR "Cannot call cmrc_add_resources in a parent directory from the resource library target")
565        return()
566    endif()
567
568    foreach(input IN LISTS ARG_UNPARSED_ARGUMENTS)
569        _cmrc_normalize_path(input)
570        get_filename_component(abs_in "${input}" ABSOLUTE)
571        # Generate a filename based on the input filename that we can put in
572        # the intermediate directory.
573        file(RELATIVE_PATH relpath "${ARG_WHENCE}" "${abs_in}")
574        if(relpath MATCHES "^\\.\\.")
575            # For now we just error on files that exist outside of the soure dir.
576            message(SEND_ERROR "Cannot add file '${input}': File must be in a subdirectory of ${ARG_WHENCE}")
577            continue()
578        endif()
579        if(DEFINED ARG_PREFIX)
580            _cmrc_normalize_path(ARG_PREFIX)
581        endif()
582        if(ARG_PREFIX AND NOT ARG_PREFIX MATCHES "/$")
583            set(ARG_PREFIX "${ARG_PREFIX}/")
584        endif()
585        get_filename_component(dirpath "${ARG_PREFIX}${relpath}" DIRECTORY)
586        _cmrc_register_dirs("${name}" "${dirpath}")
587        get_filename_component(abs_out "${libdir}/intermediate/${relpath}.cpp" ABSOLUTE)
588        # Generate a symbol name relpath the file's character array
589        _cm_encode_fpath(sym "${relpath}")
590        # Get the symbol name for the parent directory
591        if(dirpath STREQUAL "")
592            set(parent_sym root_directory)
593        else()
594            _cm_encode_fpath(parent_sym "${dirpath}")
595        endif()
596        # Generate the rule for the intermediate source file
597        _cmrc_generate_intermediate_cpp(${lib_ns} ${sym} "${abs_out}" "${abs_in}")
598        target_sources(${name} PRIVATE "${abs_out}")
599        set_property(TARGET ${name} APPEND PROPERTY CMRC_EXTERN_DECLS
600            "// Pointers to ${input}"
601            "extern const char* const ${sym}_begin\;"
602            "extern const char* const ${sym}_end\;"
603            )
604        get_filename_component(leaf "${relpath}" NAME)
605        set_property(
606            TARGET ${name}
607            APPEND PROPERTY CMRC_MAKE_FILES
608            "root_index.emplace("
609            "    \"${ARG_PREFIX}${relpath}\","
610            "    ${parent_sym}_dir.directory.add_file("
611            "        \"${leaf}\","
612            "        res_chars::${sym}_begin,"
613            "        res_chars::${sym}_end"
614            "    )"
615            ")\;"
616            )
617    endforeach()
618endfunction()
619
620function(_cmrc_generate_intermediate_cpp lib_ns symbol outfile infile)
621    add_custom_command(
622        # This is the file we will generate
623        OUTPUT "${outfile}"
624        # These are the primary files that affect the output
625        DEPENDS "${infile}" "${_CMRC_SCRIPT}"
626        COMMAND
627            "${CMAKE_COMMAND}"
628                -D_CMRC_GENERATE_MODE=TRUE
629                -DNAMESPACE=${lib_ns}
630                -DSYMBOL=${symbol}
631                "-DINPUT_FILE=${infile}"
632                "-DOUTPUT_FILE=${outfile}"
633                -P "${_CMRC_SCRIPT}"
634        COMMENT "Generating intermediate file for ${infile}"
635    )
636endfunction()
637
638function(_cm_encode_fpath var fpath)
639    string(MAKE_C_IDENTIFIER "${fpath}" ident)
640    string(MD5 hash "${fpath}")
641    string(SUBSTRING "${hash}" 0 4 hash)
642    set(${var} f_${hash}_${ident} PARENT_SCOPE)
643endfunction()
644