summaryrefslogtreecommitdiff
path: root/xmake/rules/csharp/modules
diff options
context:
space:
mode:
authorJassJam <[email protected]>2026-02-28 15:37:56 +0000
committerruki <[email protected]>2026-03-14 00:10:58 +0800
commita92e7c4087443cd91dafe14d2a5aa316a288b7c5 (patch)
treee356878da6af96a71d547906006fe45ebdbb8ed3 /xmake/rules/csharp/modules
parenta35ec2dcea9ca5c639ca9d1332a46ede16b959c0 (diff)
feat: generate csproj on demand
Diffstat (limited to 'xmake/rules/csharp/modules')
-rw-r--r--xmake/rules/csharp/modules/csharp_common.lua232
-rw-r--r--xmake/rules/csharp/modules/csproj_generator.lua301
-rw-r--r--xmake/rules/csharp/modules/itemgroups.lua166
-rw-r--r--xmake/rules/csharp/modules/properties.lua307
4 files changed, 1006 insertions, 0 deletions
diff --git a/xmake/rules/csharp/modules/csharp_common.lua b/xmake/rules/csharp/modules/csharp_common.lua
new file mode 100644
index 000000000..dce35021a
--- /dev/null
+++ b/xmake/rules/csharp/modules/csharp_common.lua
@@ -0,0 +1,232 @@
+--!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 JassJam
+-- @file csharp_common.lua
+--
+
+import("core.base.option")
+import("core.project.config")
+import("csproj_generator", {rootdir = os.scriptdir(), alias = "generate_csproj"})
+
+function _is_csharp_target(target)
+ if target:rule("csharp") then
+ return true
+ end
+ for _, sourcefile in ipairs(target:sourcefiles()) do
+ local ext = path.extension(sourcefile):lower()
+ if ext == ".cs" or ext == ".csproj" then
+ return true
+ end
+ end
+ return false
+end
+
+function _generated_csproj_path(target)
+ local targetkey = target:fullname():replace("::", path.sep())
+ local csprojdir = path.join(config.directory(), "rules", "csharp", targetkey, target:plat(), target:arch())
+ local csprojname = target:name() .. ".csproj"
+ return path.join(csprojdir, csprojname)
+end
+
+function _map_rid_arch(arch)
+ arch = (arch or ""):lower()
+ if arch == "x64" or arch == "x86_64" or arch == "amd64" then
+ return "x64"
+ elseif arch == "x86" or arch == "i386" then
+ return "x86"
+ elseif arch == "arm64" then
+ return "arm64"
+ elseif arch == "arm" or arch == "armv7" then
+ return "arm"
+ elseif arch == "riscv64" then
+ return "riscv64"
+ end
+ return nil
+end
+
+function find_csproj(target)
+ local csproj = target:data("csharp.csproj")
+ if csproj then
+ return csproj
+ end
+ for _, sourcefile in ipairs(target:sourcefiles()) do
+ if path.extension(sourcefile):lower() == ".csproj" then
+ local csprojabs = path.is_absolute(sourcefile) and sourcefile or path.absolute(sourcefile, os.projectdir())
+ if os.isfile(csprojabs) then
+ return csprojabs
+ end
+ end
+ end
+ return nil
+end
+
+function find_or_generate_csproj(target, opt)
+ opt = opt or {}
+ local csproj = find_csproj(target)
+ local generated = target:data("csharp.csproj.generated")
+ local generated_with_deps = target:data("csharp.csproj.generated.with_deps")
+
+ -- prefer existing source .csproj directly
+ if csproj and not generated then
+ return csproj
+ end
+
+ -- generated .csproj in memory cache
+ if csproj and generated then
+ if opt.skip_deps or generated_with_deps then
+ return csproj
+ end
+ end
+
+ if not _is_csharp_target(target) then
+ return nil
+ end
+
+ local csprojfile = csproj or _generated_csproj_path(target)
+ local generated_now = false
+
+ -- in load phase (skip_deps), reuse existing generated file to avoid
+ -- touching mtime and causing unnecessary rebuilds.
+ if not (opt.skip_deps and os.isfile(csprojfile)) then
+ generate_csproj(target, csprojfile, table.join(opt, {
+ is_csharp_target = _is_csharp_target,
+ find_or_generate_csproj = find_or_generate_csproj
+ }))
+ generated_now = true
+ end
+
+ target:data_set("csharp.csproj", csprojfile)
+ target:data_set("csharp.csproj.generated", true)
+ if opt.skip_deps then
+ -- keep this conservative in load phase: build/install phase will
+ -- generate a deps-enabled project if needed, and content checks avoid
+ -- unnecessary rewrites.
+ if generated_now or generated_with_deps == nil then
+ target:data_set("csharp.csproj.generated.with_deps", false)
+ end
+ else
+ target:data_set("csharp.csproj.generated.with_deps", true)
+ end
+ return csprojfile
+end
+
+function build_mode_to_configuration()
+ local mode
+ if type(get_config) == "function" then
+ mode = get_config("mode")
+ end
+ if not mode and type(is_mode) == "function" then
+ if is_mode("debug") then
+ mode = "debug"
+ elseif is_mode("release") then
+ mode = "release"
+ end
+ end
+ mode = mode or "release"
+ local mode_lower = mode:lower()
+ if mode_lower == "debug" then
+ return "Debug"
+ elseif mode_lower == "release" then
+ return "Release"
+ end
+ return mode:sub(1, 1):upper() .. mode:sub(2)
+end
+
+function get_runtime_identifier(target)
+ local rid = target:values("csharp.runtime_identifier")
+ if type(rid) == "table" then
+ rid = rid[1]
+ end
+ if rid and #rid > 0 then
+ return rid
+ end
+ local arch = _map_rid_arch(target:arch())
+ if not arch then
+ return nil
+ end
+ local plat = target:plat()
+ if plat == "windows" or plat == "mingw" or plat == "msys" or plat == "cygwin" then
+ return "win-" .. arch
+ elseif plat == "linux" then
+ return "linux-" .. arch
+ elseif plat == "macosx" then
+ return "osx-" .. arch
+ end
+ return nil
+end
+
+function append_target_flags(target, argv)
+ local flags = {}
+ table.join2(flags, table.wrap(target:get("csflags")))
+ table.join2(flags, table.wrap(target:get("ldflags")))
+ table.join2(flags, table.wrap(target:get("arflags")))
+ table.join2(flags, table.wrap(target:get("shflags")))
+ for _, flag in ipairs(flags) do
+ if flag and #flag > 0 then
+ table.insert(argv, flag)
+ end
+ end
+end
+
+function get_dotnet_runopt(csprojfile)
+ return {
+ curdir = path.directory(csprojfile),
+ envs = {
+ DOTNET_NOLOGO = "1",
+ DOTNET_CLI_TELEMETRY_OPTOUT = "1",
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1",
+ DOTNET_GENERATE_ASPNET_CERTIFICATE = "0",
+ DOTNET_ADD_GLOBAL_TOOLS_TO_PATH = "0"
+ }
+ }
+end
+
+function get_dotnet_verbosity()
+ if option.get("diagnosis") then
+ return "diagnostic"
+ end
+ return "quiet"
+end
+
+function get_dotnet_program(target)
+ local function _get_configured_program(toolkind)
+ if target:get("toolset." .. toolkind) then
+ local program = target:tool(toolkind)
+ if program and #program > 0 then
+ return program
+ end
+ end
+ end
+
+ local program = nil
+ if target:is_binary() then
+ program = _get_configured_program("ld")
+ elseif target:is_shared() then
+ program = _get_configured_program("sh")
+ else
+ program = _get_configured_program("cs")
+ end
+ if program then
+ return program
+ end
+
+ program = target:tool("cs")
+ if program and #program > 0 then
+ return program
+ end
+ return "dotnet"
+end
diff --git a/xmake/rules/csharp/modules/csproj_generator.lua b/xmake/rules/csharp/modules/csproj_generator.lua
new file mode 100644
index 000000000..0f94ed806
--- /dev/null
+++ b/xmake/rules/csharp/modules/csproj_generator.lua
@@ -0,0 +1,301 @@
+--!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 JassJam
+-- @file csproj_generator.lua
+--
+
+import("properties", {rootdir = os.scriptdir(), alias = "csharp_properties"})
+import("itemgroups", {rootdir = os.scriptdir(), alias = "csharp_itemgroups"})
+
+function _xml_escape(value)
+ value = tostring(value or "")
+ value = value:gsub("&", "&amp;")
+ value = value:gsub("<", "&lt;")
+ value = value:gsub(">", "&gt;")
+ value = value:gsub("\"", "&quot;")
+ value = value:gsub("'", "&apos;")
+ return value
+end
+
+function _format_attributes(attrs)
+ if type(attrs) ~= "table" then
+ return ""
+ end
+ local keys = {}
+ for key, value in pairs(attrs) do
+ if value ~= nil and value ~= "" then
+ table.insert(keys, key)
+ end
+ end
+ table.sort(keys)
+ if #keys == 0 then
+ return ""
+ end
+ local chunks = {}
+ for _, key in ipairs(keys) do
+ table.insert(chunks, string.format(" %s=\"%s\"", key, _xml_escape(attrs[key])))
+ end
+ return table.concat(chunks)
+end
+
+function _get_csharp_value(target, name, defaultval)
+ local val = target:values(name)
+ if type(val) == "table" then
+ val = val[1]
+ end
+ if val == nil or val == "" then
+ return defaultval
+ end
+ return val
+end
+
+function _resolve_registry_value(entry, target, context)
+ if entry.resolve then
+ return entry.resolve(context)
+ end
+ if entry.value_type == "list" then
+ local values = table.wrap(target:values(entry.lua_key))
+ if #values == 0 and entry.default ~= nil then
+ values = table.wrap(entry.default)
+ end
+ if #values > 0 then
+ local items = {}
+ for _, value in ipairs(values) do
+ if value ~= nil and value ~= "" then
+ table.insert(items, tostring(value))
+ end
+ end
+ if #items > 0 then
+ return table.concat(items, entry.sep or ";")
+ end
+ end
+ return nil
+ end
+ return _get_csharp_value(target, entry.lua_key, entry.default)
+end
+
+function _collect_project_attributes(target, context, registry_entries)
+ local attrs = {}
+ for _, entry in ipairs(registry_entries) do
+ if entry.kind == "project_attribute" then
+ if not entry.when or entry.when(context) then
+ local value = _resolve_registry_value(entry, target, context)
+ if value ~= nil and value ~= "" then
+ attrs[entry.attr] = value
+ end
+ end
+ end
+ end
+ return attrs
+end
+
+function _collect_property_entries(target, context, registry_entries)
+ local entries = {}
+ for _, entry in ipairs(registry_entries) do
+ if entry.kind == "property" then
+ if not entry.when or entry.when(context) then
+ local value = _resolve_registry_value(entry, target, context)
+ if value ~= nil and value ~= "" then
+ table.insert(entries, {xml = entry.xml, value = value})
+ end
+ end
+ end
+ end
+ return entries
+end
+
+function _normalize_item_entry(item, default_xml)
+ if type(item) == "string" then
+ return {xml = default_xml, attrs = {Include = item}}
+ elseif type(item) ~= "table" then
+ return nil
+ end
+ local xml = item.xml or default_xml
+ if not xml or #tostring(xml) == 0 then
+ return nil
+ end
+ local attrs = item.attrs
+ if type(attrs) ~= "table" then
+ attrs = {}
+ for key, value in pairs(item) do
+ if type(key) == "string" and key ~= "xml" and key ~= "value" and key ~= "attrs" then
+ attrs[key] = value
+ end
+ end
+ end
+ return {xml = xml, attrs = attrs, value = item.value}
+end
+
+function _collect_item_groups(context, registry_entries)
+ local groups = {}
+ local groupmap = {}
+ function _group(name)
+ local g = groupmap[name]
+ if not g then
+ g = {name = name, items = {}}
+ groupmap[name] = g
+ table.insert(groups, g)
+ end
+ return g
+ end
+ for _, entry in ipairs(registry_entries) do
+ if entry.kind == "item" then
+ if not entry.when or entry.when(context) then
+ for _, item in ipairs(table.wrap(entry.resolve_items and entry.resolve_items(context) or {})) do
+ local normalized = _normalize_item_entry(item, entry.xml)
+ if normalized then
+ table.insert(_group(entry.group or entry.xml).items, normalized)
+ end
+ end
+ end
+ end
+ end
+ return groups
+end
+
+function _is_valid_property_name(name)
+ return type(name) == "string" and name:match("^[A-Za-z_][A-Za-z0-9_.-]*$") ~= nil
+end
+
+function _add_custom_property(entries, name, value)
+ if not _is_valid_property_name(name) then
+ return
+ end
+ if value == nil then
+ return
+ end
+ if type(value) == "table" then
+ local values = {}
+ for _, v in ipairs(value) do
+ if v ~= nil and v ~= "" then
+ table.insert(values, tostring(v))
+ end
+ end
+ if #values == 0 then
+ return
+ end
+ value = table.concat(values, ";")
+ else
+ value = tostring(value)
+ end
+ if #value == 0 then
+ return
+ end
+ table.insert(entries, {xml = name, value = value})
+end
+
+function _add_custom_properties_from_item(entries, item)
+ if type(item) == "string" then
+ local name, value = item:match("^%s*([^=]+)%s*=(.*)$")
+ if name then
+ _add_custom_property(entries, name:trim(), value)
+ end
+ return
+ end
+ if type(item) ~= "table" then
+ return
+ end
+ if item.name then
+ _add_custom_property(entries, tostring(item.name), item.value)
+ return
+ end
+ local keys = {}
+ for k in pairs(item) do
+ if type(k) == "string" then
+ table.insert(keys, k)
+ end
+ end
+ table.sort(keys)
+ for _, key in ipairs(keys) do
+ _add_custom_property(entries, key, item[key])
+ end
+end
+
+function _collect_custom_property_entries(target)
+ local entries = {}
+ for _, item in ipairs(table.wrap(target:values("csharp.properties"))) do
+ _add_custom_properties_from_item(entries, item)
+ end
+ return entries
+end
+
+function _render_property_group(lines, entries)
+ if #entries == 0 then
+ return
+ end
+ table.insert(lines, " <PropertyGroup>")
+ for _, entry in ipairs(entries) do
+ table.insert(lines, string.format(" <%s>%s</%s>", entry.xml, _xml_escape(entry.value), entry.xml))
+ end
+ table.insert(lines, " </PropertyGroup>")
+end
+
+function _render_item_groups(lines, item_groups)
+ for _, group in ipairs(item_groups) do
+ if #group.items > 0 then
+ table.insert(lines, " <ItemGroup>")
+ for _, item in ipairs(group.items) do
+ local attrs = _format_attributes(item.attrs)
+ if item.value ~= nil and item.value ~= "" then
+ table.insert(lines, string.format(" <%s%s>%s</%s>", item.xml, attrs, _xml_escape(item.value), item.xml))
+ else
+ table.insert(lines, string.format(" <%s%s />", item.xml, attrs))
+ end
+ end
+ table.insert(lines, " </ItemGroup>")
+ end
+ end
+end
+
+function main(target, csprojfile, opt)
+ opt = opt or {}
+
+ local csprojdir = path.directory(csprojfile)
+ local context = {
+ target = target,
+ csprojfile = csprojfile,
+ csprojdir = csprojdir,
+ opt = opt
+ }
+
+ local property_registry_entries = csharp_properties()
+ local item_registry_entries = csharp_itemgroups()
+
+ local project_attributes = _collect_project_attributes(target, context, property_registry_entries)
+ local property_entries = _collect_property_entries(target, context, property_registry_entries)
+ local custom_property_entries = _collect_custom_property_entries(target)
+ table.join2(property_entries, custom_property_entries)
+
+ local item_groups = _collect_item_groups(context, item_registry_entries)
+ local lines = {}
+
+ table.insert(lines, string.format("<Project%s>", _format_attributes(project_attributes)))
+ _render_property_group(lines, property_entries)
+ _render_item_groups(lines, item_groups)
+ table.insert(lines, "</Project>")
+
+ local content = table.concat(lines, "\n") .. "\n"
+
+ os.mkdir(csprojdir)
+ local oldcontent = nil
+ if os.isfile(csprojfile) then
+ oldcontent = io.readfile(csprojfile)
+ end
+ if oldcontent ~= content then
+ io.writefile(csprojfile, content)
+ end
+end
diff --git a/xmake/rules/csharp/modules/itemgroups.lua b/xmake/rules/csharp/modules/itemgroups.lua
new file mode 100644
index 000000000..6d105fd45
--- /dev/null
+++ b/xmake/rules/csharp/modules/itemgroups.lua
@@ -0,0 +1,166 @@
+--!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 JassJam
+-- @file itemsgroups.lua
+--
+
+local function _normalize_relative(fromdir, targetpath)
+ local relpath = path.relative(targetpath, fromdir) or targetpath
+ if os.host() == "windows" then
+ relpath = relpath:gsub("\\", "/")
+ end
+ return relpath
+end
+
+local function _collect_cs_sourcefiles(context)
+ local csfiles = {}
+ for _, sourcefile in ipairs(context.target:sourcefiles()) do
+ if path.extension(sourcefile):lower() == ".cs" then
+ local sourceabs = path.is_absolute(sourcefile) and sourcefile or path.absolute(sourcefile, os.projectdir())
+ table.insert(csfiles, _normalize_relative(context.csprojdir, sourceabs))
+ end
+ end
+ table.sort(csfiles)
+ return table.unique(csfiles)
+end
+
+local function _collect_project_references(context)
+ if context.opt.skip_deps then
+ return {}
+ end
+ local references = {}
+ for _, depname in ipairs(table.wrap(context.target:get("deps"))) do
+ local dep = context.target:dep(depname)
+ if dep and context.opt.is_csharp_target and context.opt.is_csharp_target(dep) then
+ local depcsproj = context.opt.find_or_generate_csproj and context.opt.find_or_generate_csproj(dep)
+ if depcsproj then
+ table.insert(references, _normalize_relative(context.csprojdir, depcsproj))
+ end
+ end
+ end
+ table.sort(references)
+ return table.unique(references)
+end
+
+local function _get_nuget_info(pkg)
+ local requirestr = pkg:requirestr() or ""
+ local splitinfo = requirestr:trim():split("%s+")
+ if #splitinfo == 0 then
+ return nil
+ end
+
+ local pkgname = splitinfo[1]
+ if pkgname:find("::", 1, true) then
+ pkgname = pkgname:split("::", {plain = true})
+ pkgname = pkgname[#pkgname]
+ end
+ local pkgname_raw = pkgname:match("(.-)%[.*%]$")
+ if pkgname_raw and #pkgname_raw > 0 then
+ pkgname = pkgname_raw
+ end
+ if not pkgname or #pkgname == 0 then
+ return nil
+ end
+
+ local version
+ local versionobj = pkg:version()
+ if versionobj then
+ version = tostring(versionobj)
+ end
+ if not version and #splitinfo > 1 then
+ local require_version = table.concat(table.slice(splitinfo, 2), " ")
+ if require_version ~= "latest" then
+ version = require_version
+ end
+ end
+ return pkgname, version
+end
+
+local function _collect_nuget_references(context)
+ local versions = {}
+ for _, pkg in ipairs(context.target:orderpkgs()) do
+ local namespace = pkg:namespace()
+ local requirestr = pkg:requirestr() or ""
+ if namespace == "nuget" or requirestr:startswith("nuget::") then
+ local pkgname, version = _get_nuget_info(pkg)
+ if pkgname then
+ if version or versions[pkgname] == nil then
+ versions[pkgname] = version or false
+ end
+ end
+ end
+ end
+
+ local references = {}
+ for pkgname, version in pairs(versions) do
+ table.insert(references, {name = pkgname, version = version or nil})
+ end
+ table.sort(references, function (a, b) return a.name < b.name end)
+ return references
+end
+
+function main()
+ local entries = {}
+ local function register(entry)
+ table.insert(entries, entry)
+ end
+
+ register({
+ kind = "item",
+ group = "compile",
+ xml = "Compile",
+ resolve_items = function (context)
+ local items = {}
+ for _, sourcefile in ipairs(_collect_cs_sourcefiles(context)) do
+ table.insert(items, {attrs = {Include = sourcefile}})
+ end
+ return items
+ end
+ })
+
+ register({
+ kind = "item",
+ group = "project_reference",
+ xml = "ProjectReference",
+ resolve_items = function (context)
+ local items = {}
+ for _, reffile in ipairs(_collect_project_references(context)) do
+ table.insert(items, {attrs = {Include = reffile}})
+ end
+ return items
+ end
+ })
+
+ register({
+ kind = "item",
+ group = "package_reference",
+ xml = "PackageReference",
+ resolve_items = function (context)
+ local items = {}
+ for _, pkginfo in ipairs(_collect_nuget_references(context)) do
+ local attrs = {Include = pkginfo.name}
+ if pkginfo.version then
+ attrs.Version = pkginfo.version
+ end
+ table.insert(items, {attrs = attrs})
+ end
+ return items
+ end
+ })
+
+ return entries
+end
diff --git a/xmake/rules/csharp/modules/properties.lua b/xmake/rules/csharp/modules/properties.lua
new file mode 100644
index 000000000..b8d30f1e9
--- /dev/null
+++ b/xmake/rules/csharp/modules/properties.lua
@@ -0,0 +1,307 @@
+--!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 JassJam
+-- @file properties.lua
+--
+
+function _has_target_frameworks(context)
+ return #table.wrap(context.target:values("csharp.target_frameworks")) > 0
+end
+
+local _default_target_framework_cached = {}
+
+function _first(value)
+ if type(value) == "table" then
+ return value[1]
+ end
+ return value
+end
+
+function _get_target_value(target, name)
+ return _first(target:values(name))
+end
+
+function _collect_strings(result, value)
+ if value == nil then
+ return
+ end
+ if type(value) == "table" then
+ for _, item in ipairs(value) do
+ _collect_strings(result, item)
+ end
+ else
+ local sval = tostring(value)
+ if #sval > 0 then
+ table.insert(result, sval)
+ end
+ end
+end
+
+function _extract_define_symbol(define)
+ define = tostring(define):trim()
+ if #define == 0 then
+ return nil
+ end
+ local symbol = define:split("=", {plain = true})[1]
+ if not symbol then
+ return nil
+ end
+ symbol = symbol:trim()
+ if symbol:match("^[A-Za-z_][A-Za-z0-9_]*$") then
+ return symbol
+ end
+ return nil
+end
+
+function _resolve_define_constants(context)
+ local target = context.target
+ local constants = {}
+
+ _collect_strings(constants, target:get("defines"))
+ local opt_defines = target:get_from("defines", "option::*")
+ if opt_defines then
+ _collect_strings(constants, opt_defines)
+ end
+
+ local symbols = {}
+ for _, define in ipairs(constants) do
+ local symbol = _extract_define_symbol(define)
+ if symbol then
+ table.insert(symbols, symbol)
+ end
+ end
+ symbols = table.unique(symbols)
+ if #symbols > 0 then
+ return table.concat(symbols, ";")
+ end
+end
+
+function _get_default_target_framework(context)
+ local dotnet = _first(context.target:get("toolset.cs")) or "dotnet"
+ dotnet = tostring(dotnet)
+ local cached = _default_target_framework_cached[dotnet]
+ if cached ~= nil then
+ return cached
+ end
+
+ local major = nil
+ local sdks = try { function ()
+ return os.iorunv(dotnet, {"--list-sdks"})
+ end }
+ if sdks then
+ for line in sdks:gmatch("[^\r\n]+") do
+ local line_major = tonumber(line:match("^%s*(%d+)%.%d+%.%d+"))
+ if line_major and (not major or line_major > major) then
+ major = line_major
+ end
+ end
+ end
+ if not major then
+ local version = try { function ()
+ return os.iorunv(dotnet, {"--version"})
+ end }
+ if version then
+ major = tonumber(version:match("^%s*(%d+)"))
+ end
+ end
+
+ local target_framework = major and ("net" .. tostring(major) .. ".0") or "net8.0"
+ _default_target_framework_cached[dotnet] = target_framework
+ return target_framework
+end
+
+function _resolve_target_framework(context)
+ local target_framework = _get_target_value(context.target, "csharp.target_framework")
+ if target_framework ~= nil and #tostring(target_framework) > 0 then
+ return target_framework
+ end
+ return _get_default_target_framework(context)
+end
+
+function _resolve_assembly_name(context)
+ local basename = context.target:basename()
+ if basename ~= nil and #tostring(basename) > 0 then
+ return basename
+ end
+end
+
+function _resolve_optimize(context)
+ local optimize = _first(context.target:get("optimize"))
+ if optimize ~= nil then
+ return optimize == "none" and "false" or "true"
+ end
+end
+
+function _resolve_debug_symbols(context)
+ local symbols = _first(context.target:get("symbols"))
+ if symbols ~= nil then
+ return symbols == "none" and "false" or "true"
+ end
+end
+
+function _resolve_debug_type(context)
+ local symbols = _first(context.target:get("symbols"))
+ if symbols and symbols ~= "none" then
+ return "portable"
+ end
+end
+
+function _resolve_platform_target(context)
+ local arch = (context.target:arch() or ""):lower()
+ local mapping = {
+ x86_64 = "x64",
+ amd64 = "x64",
+ x64 = "x64",
+ i386 = "x86",
+ x86 = "x86",
+ arm64 = "arm64",
+ arm = "arm",
+ armv7 = "arm"
+ }
+ return mapping[arch]
+end
+
+function _resolve_warning_level(context)
+ local warnings = _first(context.target:get("warnings"))
+ local mapping = {
+ none = "0",
+ less = "2",
+ more = "3",
+ all = "4",
+ allextra = "4",
+ everything = "4",
+ error = "4"
+ }
+ return warnings and mapping[warnings]
+end
+
+function _resolve_treat_warnings_as_errors(context)
+ local warnings = _first(context.target:get("warnings"))
+ if warnings == "error" then
+ return "true"
+ end
+end
+
+function _register_property(register, suffix, xml, default, extra)
+ local entry = table.join({
+ kind = "property",
+ xml = xml,
+ lua_key = "csharp." .. suffix,
+ default = default
+ }, extra or {})
+ register(entry)
+end
+
+function _register_list_property(register, suffix, xml, extra)
+ local entry = table.join({
+ kind = "property",
+ xml = xml,
+ lua_key = "csharp." .. suffix,
+ value_type = "list",
+ sep = ";"
+ }, extra or {})
+ register(entry)
+end
+
+function main()
+ local entries = {}
+ function register(entry)
+ table.insert(entries, entry)
+ end
+
+ register({kind = "project_attribute", attr = "Sdk", lua_key = "csharp.sdk", default = "Microsoft.NET.Sdk"})
+ register({kind = "property", xml = "OutputType", resolve = function (context)
+ return context.target:is_binary() and "Exe" or "Library"
+ end})
+ _register_list_property(register, "target_frameworks", "TargetFrameworks", {when = _has_target_frameworks})
+ register({kind = "property", xml = "TargetFramework", resolve = _resolve_target_framework, when = function (context)
+ return not _has_target_frameworks(context)
+ end})
+
+ _register_property(register, "implicit_usings", "ImplicitUsings", "enable")
+ _register_property(register, "nullable", "Nullable", "enable")
+ _register_property(register, "lang_version", "LangVersion")
+ _register_property(register, "enable_default_compile_items", "EnableDefaultCompileItems", "false")
+ _register_property(register, "enable_default_embedded_resource_items", "EnableDefaultEmbeddedResourceItems")
+ _register_property(register, "enable_default_none_items", "EnableDefaultNoneItems")
+ _register_property(register, "root_namespace", "RootNamespace")
+ register({kind = "property", xml = "AssemblyName", resolve = _resolve_assembly_name})
+ _register_property(register, "generate_assembly_info", "GenerateAssemblyInfo")
+ _register_property(register, "deterministic", "Deterministic")
+ register({kind = "property", xml = "Optimize", resolve = _resolve_optimize})
+ register({kind = "property", xml = "PlatformTarget", resolve = _resolve_platform_target})
+ _register_property(register, "prefer_32bit", "Prefer32Bit")
+ _register_property(register, "allow_unsafe_blocks", "AllowUnsafeBlocks")
+ _register_property(register, "check_for_overflow_underflow", "CheckForOverflowUnderflow")
+ register({kind = "property", xml = "WarningLevel", resolve = _resolve_warning_level})
+ _register_property(register, "analysis_level", "AnalysisLevel")
+ _register_property(register, "enable_net_analyzers", "EnableNETAnalyzers")
+ _register_property(register, "enforce_code_style_in_build", "EnforceCodeStyleInBuild")
+ register({kind = "property", xml = "TreatWarningsAsErrors", resolve = _resolve_treat_warnings_as_errors})
+ _register_list_property(register, "warnings_as_errors", "WarningsAsErrors")
+ _register_list_property(register, "warnings_not_as_errors", "WarningsNotAsErrors")
+ register({kind = "property", xml = "DefineConstants", resolve = _resolve_define_constants})
+ _register_property(register, "error_log", "ErrorLog")
+ register({kind = "property", xml = "DebugType", resolve = _resolve_debug_type})
+ register({kind = "property", xml = "DebugSymbols", resolve = _resolve_debug_symbols})
+ _register_property(register, "generate_documentation_file", "GenerateDocumentationFile")
+ _register_property(register, "documentation_file", "DocumentationFile")
+
+ _register_property(register, "runtime_identifier", "RuntimeIdentifier")
+ _register_list_property(register, "runtime_identifiers", "RuntimeIdentifiers")
+ _register_property(register, "self_contained", "SelfContained")
+ _register_property(register, "use_app_host", "UseAppHost")
+ _register_property(register, "roll_forward", "RollForward")
+ _register_property(register, "publish_single_file", "PublishSingleFile")
+ _register_property(register, "publish_trimmed", "PublishTrimmed")
+ _register_property(register, "trim_mode", "TrimMode")
+ _register_property(register, "publish_ready_to_run", "PublishReadyToRun")
+ _register_property(register, "invariant_globalization", "InvariantGlobalization")
+ _register_property(register, "include_native_libraries_for_self_extract", "IncludeNativeLibrariesForSelfExtract")
+ _register_property(register, "enable_compression_in_single_file", "EnableCompressionInSingleFile")
+ _register_property(register, "publish_aot", "PublishAot")
+ _register_property(register, "strip_symbols", "StripSymbols")
+ _register_property(register, "enable_trim_analyzer", "EnableTrimAnalyzer")
+ _register_property(register, "json_serializer_is_reflection_enabled_by_default", "JsonSerializerIsReflectionEnabledByDefault")
+ _register_list_property(register, "satellite_resource_languages", "SatelliteResourceLanguages")
+
+ _register_property(register, "version", "Version")
+ _register_property(register, "assembly_version", "AssemblyVersion")
+ _register_property(register, "file_version", "FileVersion")
+ _register_property(register, "informational_version", "InformationalVersion")
+ _register_property(register, "package_id", "PackageId")
+ _register_property(register, "authors", "Authors")
+ _register_property(register, "company", "Company")
+ _register_property(register, "product", "Product")
+ _register_property(register, "description", "Description")
+ _register_property(register, "copyright", "Copyright")
+ _register_property(register, "repository_url", "RepositoryUrl")
+ _register_property(register, "repository_type", "RepositoryType")
+ _register_property(register, "package_license_expression", "PackageLicenseExpression")
+ _register_property(register, "package_project_url", "PackageProjectUrl")
+ _register_property(register, "neutral_language", "NeutralLanguage")
+ _register_property(register, "enable_preview_features", "EnablePreviewFeatures")
+
+ _register_property(register, "generate_runtime_configuration_files", "GenerateRuntimeConfigurationFiles")
+ _register_property(register, "copy_local_lock_file_assemblies", "CopyLocalLockFileAssemblies")
+ _register_property(register, "append_target_framework_to_output_path", "AppendTargetFrameworkToOutputPath", "false")
+ _register_property(register, "append_runtime_identifier_to_output_path", "AppendRuntimeIdentifierToOutputPath", "false")
+ _register_property(register, "produce_reference_assembly", "ProduceReferenceAssembly")
+ _register_property(register, "disable_implicit_framework_references", "DisableImplicitFrameworkReferences")
+ _register_property(register, "generate_target_framework_attribute", "GenerateTargetFrameworkAttribute")
+ return entries
+end