--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki -- @file meson.lua -- -- imports import("core.base.option") import("core.project.config") import("core.tool.toolchain") import("lib.detect.find_tool") import("private.utils.executable_path") import("private.utils.toolchain", {alias = "toolchain_utils"}) -- get build directory function _get_builddir(package, opt) if opt and (opt.builddir or opt.buildir) then if opt.buildir then wprint("{buildir = } has been deprecated, please use {builddir = } in meson.install") end return opt.builddir or opt.buildir else _g.builddir = _g.builddir or package:builddir() return _g.builddir end end -- get pkg-config, we need force to find it, because package install environments will be changed function _get_pkgconfig(package) -- meson need fullpath pkgconfig -- @see https://github.com/xmake-io/xmake/issues/5474 local dep = package:dep("pkgconf") or package:dep("pkg-config") if dep then local suffix = dep:is_plat("windows", "mingw") and ".exe" or "" local pkgconf = path.join(dep:installdir("bin"), "pkgconf" .. suffix) if os.isfile(pkgconf) then return pkgconf end local pkgconfig = path.join(dep:installdir("bin"), "pkg-config" .. suffix) if os.isfile(pkgconfig) then return pkgconfig end end if package:is_plat("windows") then local pkgconf = find_tool("pkgconf", {force = true}) if pkgconf then return pkgconf.program end end local pkgconfig = find_tool("pkg-config", {force = true}) if pkgconfig then return pkgconfig.program end end -- translate flags function _translate_flags(package, flags) if package:is_plat("android") then local flags_new = {} for _, flag in ipairs(flags) do if flag:startswith("--gcc-toolchain=") or flag:startswith("--target=") or flag:startswith("-isystem ") then table.join2(flags_new, flag:split(" ", {limit = 2})) else table.insert(flags_new, flag) end end flags = flags_new elseif package:is_plat("windows") then for idx, flag in ipairs(flags) do -- @see https://github.com/xmake-io/xmake/issues/4407 if flag:startswith("-libpath:") then flags[idx] = flag:gsub("%-libpath:", "/libpath:") end end end return flags end function _insert_cross_configs(package, file, opt) -- host machine file:print("[host_machine]") if opt.host_machine then file:print("%s", opt.host_machine) elseif package:is_plat("iphoneos", "macosx") then local cpu local cpu_family if package:is_arch("arm64") then cpu = "aarch64" cpu_family = "aarch64" elseif package:is_arch("armv7") then cpu = "arm" cpu_family = "arm" elseif package:is_arch("x64", "x86_64") then cpu = "x86_64" cpu_family = "x86_64" elseif package:is_arch("x86", "i386") then cpu = "i686" cpu_family = "x86" else raise("unsupported arch(%s)", package:arch()) end file:print("system = 'darwin'") file:print("cpu_family = '%s'", cpu_family) file:print("cpu = '%s'", cpu) file:print("endian = 'little'") elseif package:is_plat("android") then local cpu local cpu_family if package:is_arch("arm64-v8a") then cpu = "aarch64" cpu_family = "aarch64" elseif package:is_arch("armeabi-v7a") then cpu = "arm" cpu_family = "arm" elseif package:is_arch("x64", "x86_64") then cpu = "x86_64" cpu_family = "x86_64" elseif package:is_arch("x86", "i386") then cpu = "i686" cpu_family = "x86" else raise("unsupported arch(%s)", package:arch()) end file:print("system = 'android'") file:print("cpu_family = '%s'", cpu_family) file:print("cpu = '%s'", cpu) file:print("endian = 'little'") elseif package:is_plat("mingw") then local cpu local cpu_family if package:is_arch("x64", "x86_64") then cpu = "x86_64" cpu_family = "x86_64" elseif package:is_arch("x86", "i386") then cpu = "i686" cpu_family = "x86" else raise("unsupported arch(%s)", package:arch()) end file:print("system = 'windows'") file:print("cpu_family = '%s'", cpu_family) file:print("cpu = '%s'", cpu) file:print("endian = 'little'") elseif package:is_plat("windows") then local cpu local cpu_family if package:is_arch("arm64", "arm64ec") then cpu = "aarch64" cpu_family = "aarch64" elseif package:is_arch("x86") then cpu = "x86" cpu_family = "x86" elseif package:is_arch("x64") then cpu = "x86_64" cpu_family = "x86_64" else raise("unsupported arch(%s)", package:arch()) end file:print("system = 'windows'") file:print("cpu_family = '%s'", cpu_family) file:print("cpu = '%s'", cpu) file:print("endian = 'little'") elseif package:is_plat("wasm") then file:print("system = 'emscripten'") file:print("cpu_family = '%s'", package:arch()) file:print("cpu = '%s'", package:arch()) file:print("endian = 'little'") else local cpu = package:arch() if package:is_arch("arm64") or package:is_arch("aarch64") then cpu = "aarch64" elseif package:is_arch("arm.*") then cpu = "arm" end local cpu_family = cpu file:print("system = '%s'", package:targetos() or "linux") file:print("cpu_family = '%s'", cpu_family) file:print("cpu = '%s'", cpu) file:print("endian = 'little'") end end -- is the toolchain compatible with the host? function _is_toolchain_compatible_with_host(package) for _, name in ipairs(package:config("toolchains")) do if toolchain_utils.is_compatible_with_host(name) then return true end end end -- get cross file function _get_configs_file(package, opt) opt = opt or {} local configsfile = path.join(_get_builddir(package, opt), "configs_file.txt") if not os.isfile(configsfile) then local file = io.open(configsfile, "w") -- binaries file:print("[binaries]") local cc = package:build_getenv("cc") if cc then file:print("c=['%s']", executable_path(cc)) end local cxx = package:build_getenv("cxx") if cxx then -- https://github.com/xmake-io/xmake/discussions/4979 if package:has_tool("cxx", "clang", "gcc") then local dir = path.directory(cxx) local name = path.filename(cxx) name = name:gsub("clang$", "clang++") name = name:gsub("clang%-", "clang++-") -- clang-xx name = name:gsub("clang%.", "clang++.") -- clang.exe name = name:gsub("gcc$", "g++") name = name:gsub("gcc%-", "g++-") name = name:gsub("gcc%.", "g++.") if dir and dir ~= "." then cxx = path.join(dir, name) else cxx = name end end file:print("cpp=['%s']", executable_path(cxx)) end local ld = package:build_getenv("ld") if ld then file:print("ld=['%s']", executable_path(ld)) end -- we cannot pass link.exe to ar for msvc, it will raise `unknown linker` if not package:is_plat("windows") then local ar = package:build_getenv("ar") if ar then file:print("ar=['%s']", executable_path(ar)) end end local strip = package:build_getenv("strip") if strip then file:print("strip=['%s']", executable_path(strip)) end local ranlib = package:build_getenv("ranlib") if ranlib then file:print("ranlib=['%s']", executable_path(ranlib)) end if package:is_plat("mingw", "msys", "windows") then local mrc = package:build_getenv("mrc") if mrc then file:print("windres=['%s']", executable_path(mrc)) end local dlltool = package:build_getenv("dlltool") if dlltool then file:print("dlltool=['%s']", executable_path(dlltool)) end end local cmake = find_tool("cmake") if cmake then file:print("cmake=['%s']", executable_path(cmake.program)) end local pkgconfig = _get_pkgconfig(package) if pkgconfig then file:print("pkgconfig=['%s']", executable_path(pkgconfig)) end file:print("") -- built-in options file:print("[built-in options]") local cflags = table.join(table.wrap(package:build_getenv("cxflags")), package:build_getenv("cflags")) local cxxflags = table.join(table.wrap(package:build_getenv("cxflags")), package:build_getenv("cxxflags")) local asflags = table.wrap(package:build_getenv("asflags")) local arflags = table.wrap(package:build_getenv("arflags")) local ldflags = table.wrap(package:build_getenv("ldflags")) local shflags = table.wrap(package:build_getenv("shflags")) table.join2(cflags, opt.cflags) table.join2(cflags, opt.cxflags) table.join2(cxxflags, opt.cxxflags) table.join2(cxxflags, opt.cxflags) table.join2(asflags, opt.asflags) table.join2(ldflags, opt.ldflags) table.join2(shflags, opt.shflags) table.join2(cflags, _get_cflags_from_packagedeps(package, opt)) table.join2(cxxflags, _get_cflags_from_packagedeps(package, opt)) table.join2(ldflags, _get_ldflags_from_packagedeps(package, opt)) table.join2(shflags, _get_ldflags_from_packagedeps(package, opt)) -- add runtimes flags for _, runtime in ipairs(package:runtimes()) do if not runtime:startswith("M") then table.join2(cxxflags, toolchain_utils.map_compflags_for_package(package, "cxx", "runtime", {runtime})) table.join2(ldflags, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "runtime", {runtime})) table.join2(shflags, toolchain_utils.map_linkflags_for_package(package, "shared", {"cxx"}, "runtime", {runtime})) end end if #cflags > 0 then file:print("c_args=['%s']", table.concat(cflags, "', '")) end if #cxxflags > 0 then file:print("cpp_args=['%s']", table.concat(cxxflags, "', '")) end local linkflags = table.join(ldflags or {}, shflags) if #linkflags > 0 then file:print("c_link_args=['%s']", table.concat(linkflags, "', '")) file:print("cpp_link_args=['%s']", table.concat(linkflags, "', '")) end if package:is_cross() or opt.cross then file:print("") _insert_cross_configs(package, file, opt) file:print("") end file:close() end return configsfile end -- get configs function _get_configs(package, configs, opt) -- add prefix -- when cross-compiling on Windows to a Unix-like target (e.g. Android), Meson validates -- the prefix as a Unix absolute path and rejects Windows paths like C:/... Use prefix=/ -- and redirect installation via DESTDIR in the install step instead. configs = configs or {} opt._configs_str = string.serialize(configs, {indent = false, strip = true}) if is_host("windows") and package:is_cross() and not package:is_plat("windows", "mingw") then table.insert(configs, "--prefix=/") else table.insert(configs, "--prefix=" .. path.unix(opt.prefix or package:installdir())) end table.insert(configs, "--libdir=lib") -- set build type table.insert(configs, "-Dbuildtype=" .. (package:debug() and "debug" or "release")) -- add -fpic if package:is_plat("linux") and package:config("pic") ~= false then table.insert(configs, "-Db_staticpic=true") end -- add lto if package:config("lto") then table.insert(configs, "-Db_lto=true") end -- add asan if package:config("asan") then table.insert(configs, "-Db_sanitize=address") end -- add library kind if package:is_library() then local has_already_libflag = opt._configs_str and opt._configs_str:find("default_library", 1, true) if not has_already_libflag then table.insert(configs, "-Ddefault_library=".. (package:config("shared") and "shared" or "static")) end end -- add vs runtimes flags if package:is_plat("windows") then table.insert(configs, "--vsenv") if package:has_runtime("MT") then table.insert(configs, "-Db_vscrt=mt") elseif package:has_runtime("MTd") then table.insert(configs, "-Db_vscrt=mtd") elseif package:has_runtime("MD") then table.insert(configs, "-Db_vscrt=md") elseif package:has_runtime("MDd") then table.insert(configs, "-Db_vscrt=mdd") end end -- add cross file if package:is_cross() or package:is_plat("mingw") then table.insert(configs, "--cross-file=" .. _get_configs_file(package, opt)) elseif package:config("toolchains") then if _is_toolchain_compatible_with_host(package) then table.insert(configs, "--native-file=" .. _get_configs_file(package, opt)) else table.insert(configs, "--cross-file=" .. _get_configs_file(package, table.join2(opt, {cross = true}))) end end -- add build directory table.insert(configs, _get_builddir(package, opt)) return configs end -- get msvc function _get_msvc(package) local msvc = package:toolchain("msvc") assert(msvc:check(), "vs not found!") -- we need to check vs envs if it has been not checked yet return msvc end -- get msvc run environments function _get_msvc_runenvs(package) return os.joinenvs(_get_msvc(package):runenvs()) end -- fix libname on windows function _fix_libname_on_windows(package) for _, lib in ipairs(os.files(path.join(package:installdir("lib"), "lib*.a"))) do os.mv(lib, (lib:gsub("(.+)\\lib(.-)%.a", "%1\\%2.lib"))) end end -- get cflags from package deps function _get_cflags_from_packagedeps(package, opt) local values for _, depname in ipairs(opt.packagedeps) do local dep = type(depname) ~= "string" and depname or package:librarydep(depname) if dep then local fetchinfo = dep:fetch() if fetchinfo then if values then values = values .. fetchinfo else values = fetchinfo end end end end -- @see https://github.com/xmake-io/xmake-repo/pull/4973#issuecomment-2295890196 local result = {} if values then if values.defines then table.join2(result, toolchain_utils.map_compflags_for_package(package, "cxx", "define", values.defines)) end if values.includedirs then table.join2(result, toolchain_utils.map_compflags_for_package(package, "cxx", "includedir", values.includedirs)) end if values.sysincludedirs then table.join2(result, toolchain_utils.map_compflags_for_package(package, "cxx", "sysincludedir", values.sysincludedirs)) end end return _translate_flags(package, result) end -- get ldflags from package deps function _get_ldflags_from_packagedeps(package, opt) local values for _, depname in ipairs(opt.packagedeps) do local dep = type(depname) ~= "string" and depname or package:librarydep(depname) if dep then local fetchinfo = dep:fetch() if fetchinfo then if values then values = values .. fetchinfo else values = fetchinfo end end end end local result = {} if values then if values.linkdirs then table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "linkdir", values.linkdirs)) end if values.links then table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "link", values.links)) end if values.syslinks then table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "syslink", values.syslinks)) end if values.frameworks then table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "framework", values.frameworks)) end end return _translate_flags(package, result) end -- get the build environments function buildenvs(package, opt) local envs = {} if not is_host("windows") and package:is_plat("windows") then local msvc = package:toolchain("msvc") or package:toolchain("clang") or package:toolchain("clang-cl") assert(msvc:check(), "msvc envs not found!") -- we need to check vs envs if it has been not checked yet envs = os.joinenvs(msvc:runenvs()) end opt = opt or {} if package:is_plat(os.host()) then local cflags = table.join(table.wrap(package:config("cxflags")), package:config("cflags")) local cxxflags = table.join(table.wrap(package:config("cxflags")), package:config("cxxflags")) local asflags = table.wrap(package:config("asflags")) local ldflags = table.wrap(package:config("ldflags")) local shflags = table.wrap(package:config("shflags")) table.join2(cflags, opt.cflags) table.join2(cflags, opt.cxflags) table.join2(cxxflags, opt.cxxflags) table.join2(cxxflags, opt.cxflags) table.join2(asflags, opt.asflags) table.join2(ldflags, opt.ldflags) table.join2(shflags, opt.shflags) table.join2(cflags, _get_cflags_from_packagedeps(package, opt)) table.join2(cxxflags, _get_cflags_from_packagedeps(package, opt)) table.join2(ldflags, _get_ldflags_from_packagedeps(package, opt)) table.join2(shflags, _get_ldflags_from_packagedeps(package, opt)) envs.CFLAGS = table.concat(cflags, ' ') envs.CXXFLAGS = table.concat(cxxflags, ' ') envs.ASFLAGS = table.concat(asflags, ' ') envs.LDFLAGS = table.concat(ldflags, ' ') envs.SHFLAGS = table.concat(shflags, ' ') if package:is_plat("windows") then envs = os.joinenvs(envs, _get_msvc_runenvs(package)) local pkgconf = _get_pkgconfig(package) if pkgconf then envs.PKG_CONFIG = pkgconf end end end local ACLOCAL_PATH = {} local PKG_CONFIG_PATH = {} local CMAKE_LIBRARY_PATH = {} local CMAKE_INCLUDE_PATH = {} local CMAKE_PREFIX_PATH = {} for _, dep in ipairs(package:librarydeps({private = true})) do local pkgconfigs = {path.join(dep:installdir(), "lib", "pkgconfig")} if package:is_plat("bsd") then table.insert(pkgconfigs, path.join(dep:installdir(), "libdata", "pkgconfig")) end for _, pkgconfig in ipairs(pkgconfigs) do if os.isdir(pkgconfig) then table.insert(PKG_CONFIG_PATH, pkgconfig) end end pkgconfig = path.join(dep:installdir(), "share", "pkgconfig") if os.isdir(pkgconfig) then table.insert(PKG_CONFIG_PATH, pkgconfig) end -- meson may also use cmake to find dependencies if dep:is_system() then local fetchinfo = dep:fetch() if fetchinfo then table.join2(CMAKE_LIBRARY_PATH, fetchinfo.linkdirs) table.join2(CMAKE_INCLUDE_PATH, fetchinfo.includedirs) table.join2(CMAKE_INCLUDE_PATH, fetchinfo.sysincludedirs) end else table.join2(CMAKE_PREFIX_PATH, dep:installdir()) end end -- some binary packages contain it too. e.g. libtool for _, dep in ipairs(package:orderdeps()) do local aclocal = path.join(dep:installdir(), "share", "aclocal") if os.isdir(aclocal) then table.insert(ACLOCAL_PATH, aclocal) end end envs.ACLOCAL_PATH = path.joinenv(ACLOCAL_PATH) envs.CMAKE_LIBRARY_PATH = path.joinenv(CMAKE_LIBRARY_PATH) envs.CMAKE_INCLUDE_PATH = path.joinenv(CMAKE_INCLUDE_PATH) envs.CMAKE_PREFIX_PATH = path.joinenv(CMAKE_PREFIX_PATH) envs.PKG_CONFIG_PATH = path.joinenv(PKG_CONFIG_PATH) return envs end -- generate build files for ninja function generate(package, configs, opt) -- init options opt = opt or {} -- pass configurations -- TODO: support more backends https://mesonbuild.com/Commands.html#setup local argv = {"setup"} for name, value in pairs(_get_configs(package, configs, opt)) do local value = tostring(value):trim() if value ~= "" then if type(name) == "number" then table.insert(argv, value) else table.insert(argv, "--" .. name .. "=" .. value) end end end -- do configure local meson = assert(find_tool("meson"), "meson not found!") os.vrunv(meson.program, argv, {envs = opt.envs or buildenvs(package, opt)}) end -- build package function build(package, configs, opt) -- generate build files opt = opt or {} generate(package, configs, opt) -- configurate build local builddir = _get_builddir(package, opt) local njob = opt.jobs or option.get("jobs") or tostring(os.default_njob()) local argv = {"compile", "-C", builddir} if option.get("diagnosis") then table.insert(argv, "-v") end table.insert(argv, "-j") table.insert(argv, njob) -- do build local meson = assert(find_tool("meson"), "meson not found!") os.vrunv(meson.program, argv, {envs = opt.envs or buildenvs(package, opt)}) end -- install package function install(package, configs, opt) -- do build opt = opt or {} build(package, configs, opt) -- configure install local builddir = _get_builddir(package, opt) local argv = {"install", "-C", builddir} -- when prefix=/ was used for cross-builds on Windows, redirect the install to the -- actual package directory via DESTDIR so files land in the right place. -- meson's destdir_join strips the leading / from prefix, giving DESTDIR/lib, DESTDIR/include, etc. if is_host("windows") and package:is_cross() and not package:is_plat("windows", "mingw") then table.insert(argv, "--destdir") table.insert(argv, opt.prefix or package:installdir()) end -- do install local meson = assert(find_tool("meson"), "meson not found!") os.vrunv(meson.program, argv, {envs = opt.envs or buildenvs(package, opt)}) -- fix static libname on windows if package:is_plat("windows") and not package:config("shared") then _fix_libname_on_windows(package) end end