mirror of https://github.com/nmlgc/ReC98.git
273 lines
8.5 KiB
Lua
273 lines
8.5 KiB
Lua
---@alias TupInput string | { [1]: string, extra_inputs: string | string[] }
|
|
---@alias TupOutput string | { [1]: string, extra_outputs: string | string[] }
|
|
|
|
tup.import("ReC98_DOS=")
|
|
|
|
---@class (exact) Rule
|
|
---@field inputs string[]
|
|
---@field tool string
|
|
---@field args string
|
|
|
|
---ReC98-specific reimplementation of `tup generate` in Lua. Wraps `tup.rule()`
|
|
---and transforms the rules into a full dumb sequential batch file, but with
|
|
---the following benefits to create an ideal native 16-/32-bit build:
|
|
---
|
|
---* Automatic slash-to-backslash conversion for tool filenames
|
|
---* Emits `mkdir` commands for *all* referenced output directories, not just
|
|
--- the ones that are missing from the current working tree.
|
|
---* Bundles all single-file TCC invocations that share the same compiler flags
|
|
--- into a single batched one
|
|
---* Splits long `echo` lines to fit within Windows 9x limits.
|
|
---
|
|
---Since Tup forbids writing files from the Tupfile, we have to abuse stdout
|
|
---to get the transformed rules back into the repo. All rule lines are printed
|
|
---with a fixed prefix, which is then filtered and redirected into the final
|
|
---batch file inside `build.bat`.
|
|
---@class Rules
|
|
---@field private rules Rule[]
|
|
---@field private outdirs table<string, boolean> Set of output directories
|
|
---@field private batch_root string Root directory for TCC batch response files
|
|
---@field private tcc_batch table<string, Rule> Command-line arguments → rule that receives all input files compiled with these arguments
|
|
---@field private tcc_batch_count number Because # only works with sequential tables
|
|
local Rules = {}
|
|
Rules.__index = Rules
|
|
|
|
---@param batch_root string
|
|
function NewRules(batch_root)
|
|
---@type Rules
|
|
local ret = {
|
|
rules = {},
|
|
outdirs = {},
|
|
batch_root = batch_root,
|
|
tcc_batch = {},
|
|
tcc_batch_count = 0,
|
|
}
|
|
return setmetatable(ret, Rules)
|
|
end
|
|
|
|
---@private
|
|
---@param inputs TupInput
|
|
---@param tool string
|
|
---@param args string
|
|
---@param outputs TupOutput
|
|
---@return string cmd
|
|
function Rules:insert(inputs, tool, args, outputs)
|
|
---@type string[]
|
|
local ins = {}
|
|
---@type string[]
|
|
local outs = {}
|
|
|
|
-- Using the function instead of `+=` preserves the `string[]` type in the
|
|
-- LSP.
|
|
tup_append_assignment(ins, inputs)
|
|
tup_append_assignment(outs, outputs)
|
|
|
|
local replace_table = { f = inputs, i = inputs.extra_inputs }
|
|
args = args:gsub("%%(%d+)([fi])", function (i, type)
|
|
return replace_table[type][tonumber(i)]
|
|
end)
|
|
args = args:gsub("%%o", table.concat(outs, " "))
|
|
|
|
tup_append_assignment(outs, outputs.extra_outputs)
|
|
for _, output in ipairs(outs) do
|
|
local dir = output:gsub("/", "\\"):match("(.*)\\")
|
|
if (dir ~= nil) then
|
|
self.outdirs[dir] = true
|
|
end
|
|
end
|
|
|
|
if (tool == "tcc") then
|
|
local batch = self.tcc_batch[args]
|
|
if batch == nil then
|
|
local rsp_fn = string.format(
|
|
"%sbatch%03d.@c", self.batch_root, self.tcc_batch_count
|
|
)
|
|
batch = {
|
|
inputs = ins,
|
|
tool = "echo",
|
|
args = string.format("%s>%s", args, rsp_fn:gsub("/", "\\")),
|
|
}
|
|
table.insert(self.rules, batch)
|
|
table.insert(self.rules, {
|
|
inputs = { rsp_fn }, tool = "tcc", args = "@%f",
|
|
})
|
|
self.tcc_batch[args] = batch
|
|
self.tcc_batch_count = (self.tcc_batch_count + 1)
|
|
else
|
|
batch.inputs += ins
|
|
end
|
|
else
|
|
local rule = { inputs = ins, tool = tool, args = args }
|
|
table.insert(self.rules, rule)
|
|
end
|
|
return string.format("%s %s", tool, args)
|
|
end
|
|
|
|
---Adds a rule for a 32-bit build tool. Runs natively in both Tup and the batch
|
|
---file.
|
|
---@param inputs TupInput
|
|
---@param tool string
|
|
---@param args string
|
|
---@param outputs TupOutput
|
|
---@return string[]
|
|
function Rules:add_32(inputs, tool, args, outputs)
|
|
return tup.rule(inputs, self:insert(inputs, tool, args, outputs), outputs)
|
|
end
|
|
|
|
---Adds a rule for a 16-bit DOS build tool. Runs in `ReC98_DOS` inside Tup, and
|
|
---natively in the batch file.
|
|
---@param inputs TupInput
|
|
---@param tool string
|
|
---@param args string
|
|
---@param outputs TupOutput
|
|
---@return string[]
|
|
function Rules:add_16(inputs, tool, args, outputs)
|
|
local cmd = self:insert(inputs, tool, args, outputs)
|
|
return tup.rule(inputs, (ReC98_DOS .. " " .. cmd), outputs)
|
|
end
|
|
|
|
---Splits `line` into multiple lines to work around the no longer documented
|
|
---parsing craziness that Windows 9x's `COMMAND.COM` blindly performs before
|
|
---executing any line of a batch file:
|
|
---
|
|
---* Truncate to 1023 bytes
|
|
---* Parse out redirector
|
|
---* Strip trailing whitespace of remaining line
|
|
---* Split line into up to 64 tokens, around/excluding whitespace and '=' and
|
|
--- before/including '/'
|
|
---* Throw a `Bad command or file name` error once the token count would reach
|
|
--- 65, regardless of whether the command would actually need the tokens
|
|
---@param line string
|
|
---@param sub_bytes number | nil Subtracted from 1023.
|
|
---@param sub_args number | nil Subtracted from 64.
|
|
---@return fun(): string | nil,boolean it Iterator over the split lines, plus an indicator of whether the current string is the final one
|
|
local function split_batch_line_for_win9x(line, sub_bytes, sub_args)
|
|
local max_bytes = (1023 - ((sub_bytes and sub_bytes) or 0))
|
|
local max_args = (64 - ((sub_args and sub_args) or 0))
|
|
local switchar = string.byte("/")
|
|
local equals = string.byte("=")
|
|
|
|
---@param chunk string
|
|
local function cut(chunk)
|
|
---@type integer | nil
|
|
local prev_end = nil
|
|
|
|
local argc = 0
|
|
local cur_start, cur_end = chunk:find("%S+")
|
|
while ((cur_start ~= nil) and (cur_end ~= nil)) do
|
|
local cur_argc = 0
|
|
for i = (cur_start + 1), (cur_end - 1) do
|
|
local c = chunk:byte(i)
|
|
if ((c == switchar) or (c == equals)) then
|
|
cur_argc = (cur_argc + 1)
|
|
|
|
-- Subtracting 1 because the surrounding word may or may
|
|
-- not be OK.
|
|
if ((argc + cur_argc) >= (max_args - 1)) then
|
|
-- Back up to the previous word if we can…
|
|
if (prev_end and (cur_argc < max_args)) then
|
|
return chunk:sub(1, prev_end)
|
|
end
|
|
|
|
-- …or split the string inside the word. Only here for
|
|
-- tests, as it would corrupt the response files if it
|
|
-- ever happened.
|
|
return chunk:sub(1, (i - 1))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Separator characters are fine? Yield up to the entire word
|
|
argc = (argc + cur_argc + 1)
|
|
if (argc >= max_args) then
|
|
return chunk:sub(1, cur_end)
|
|
end
|
|
prev_end = cur_end
|
|
cur_start, cur_end = chunk:find("%S+", (cur_end + 1))
|
|
end
|
|
return chunk
|
|
end
|
|
|
|
---@type string
|
|
line = line:match("%s*(.*)%s*$")
|
|
|
|
return function ()
|
|
if (#line == 0) then
|
|
return nil, true
|
|
end
|
|
|
|
local chunk
|
|
if (#line > max_bytes) then
|
|
chunk = cut(line:sub(0, (max_bytes + 1)):match("(.+) "))
|
|
else
|
|
chunk = cut(line:sub(0, max_bytes))
|
|
end
|
|
line = line:sub(chunk:len() + 1):match("%s*(.*)%s*$")
|
|
return chunk, (#line == 0)
|
|
end
|
|
end
|
|
|
|
---Prints the current rules to stdout.
|
|
function Rules:print()
|
|
print([[
|
|
$ : Dumb, full batch build script for 32-bit systems that can't run Tup, but can
|
|
$ : natively run DOS-based tools. Automatically generated whenever `Tupfile.lua`
|
|
$ : is modified.
|
|
$ @echo off
|
|
]])
|
|
local outdirs_sorted = {}
|
|
for dir in pairs(self.outdirs) do
|
|
table.insert(outdirs_sorted, dir)
|
|
end
|
|
table.sort(outdirs_sorted)
|
|
for _, dir in pairs(outdirs_sorted) do
|
|
print(string.format("$ mkdir %s %%STDERR_IGNORE%%", dir))
|
|
end
|
|
|
|
for _, rule in pairs(self.rules) do
|
|
local cmd = rule.args:gsub("%%f", table.concat(rule.inputs, " "))
|
|
|
|
-- Work around command line length limits by splitting long `echo`
|
|
-- lines. This also must be done for the link response files used for
|
|
-- the regular Tup build.
|
|
local ECHO = "echo"
|
|
if (rule.tool == ECHO) then
|
|
local redirector = ">"
|
|
|
|
---@type string, string
|
|
local args, fn = cmd:match("(.*)>(.+)")
|
|
|
|
local rsp_ext = fn:match(".@(.)$")
|
|
|
|
-- Two spaces rather than one are, in fact, necessary to increase
|
|
-- the stability of TCC's optimizer in very rare cases? Not doing
|
|
-- this ends up inserting a bunch of useless instructions into the
|
|
-- TH01 Elis translation unit, but only on 32-bit Windows 10.
|
|
-- Really, I couldn't make this up.
|
|
if (rsp_ext == "c") then
|
|
args = args:gsub(" ", " ")
|
|
end
|
|
|
|
-- Adding 2 to leave room for the longer `>>` redirector
|
|
for chunk, final in split_batch_line_for_win9x(
|
|
args, (ECHO:len() + 1 + fn:len() + 2), 1
|
|
) do
|
|
-- TLINK response files need some separate massaging when split
|
|
-- into multiple lines.
|
|
if (rsp_ext == "l") then
|
|
if chunk:sub(-1) == "," then
|
|
chunk = chunk:sub(0, (chunk:len() - 1))
|
|
elseif not final then
|
|
chunk = (chunk .. "+")
|
|
end
|
|
end
|
|
|
|
print(string.format("$ %s %s%s%s", ECHO, chunk, redirector, fn))
|
|
redirector = ">>"
|
|
end
|
|
else
|
|
print(string.format("$ %s %s", rule.tool:gsub("/", "\\"), cmd))
|
|
end
|
|
end
|
|
end
|