1#!/usr/bin/env lua
2
3-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
4--
5-- SPDX-License-Identifier: GPL-3.0-or-later
6
7--[[
8Bundle script for wt - concatenates Lua modules into a single distributable file.
9
10The bundled output embeds each module's source and provides a custom require()
11that loads from the embedded sources first, falling back to the real require()
12for standard library modules.
13
14Usage: lua scripts/bundle.lua > wt
15]]
16
17--- Read entire file contents
18---@param path string
19---@return string|nil content
20---@return string|nil error
21local function read_file(path)
22 local f, err = io.open(path, "r")
23 if not f then
24 return nil, err
25 end
26 local content = f:read("*a")
27 f:close()
28 return content
29end
30
31--- Check if file exists
32---@param path string
33---@return boolean
34local function file_exists(path)
35 local f = io.open(path, "r")
36 if f then
37 f:close()
38 return true
39 end
40 return false
41end
42
43--- Determine appropriate long string delimiters for embedding
44---@param s string
45---@return string open delimiter
46---@return string close delimiter
47---@return string content (unchanged)
48local function escape_for_longstring(s)
49 local level = 0
50 while s:find("]" .. string.rep("=", level) .. "]", 1, true) do
51 level = level + 1
52 end
53 local open = "[" .. string.rep("=", level) .. "["
54 local close = "]" .. string.rep("=", level) .. "]"
55 return open, close, s
56end
57
58-- Module loading order matters: dependencies must come before dependents
59-- This will be populated as modules are extracted
60local MODULE_ORDER = {
61 -- Core utilities (no internal dependencies)
62 "wt.exit", -- exit codes
63 "wt.shell", -- run_cmd, run_cmd_silent
64 "wt.path", -- path manipulation utilities
65 "wt.git", -- git utilities
66 "wt.config", -- config loading
67 "wt.hooks", -- hook system
68 "wt.help", -- help text
69 -- Commands (depend on utilities)
70 "wt.cmd.clone",
71 "wt.cmd.new",
72 "wt.cmd.add",
73 "wt.cmd.remove",
74 "wt.cmd.list",
75 "wt.cmd.fetch",
76 "wt.cmd.init",
77}
78
79-- Map module names to file paths
80local MODULE_PATHS = {
81 ["wt.exit"] = "src/wt/exit.lua",
82 ["wt.shell"] = "src/wt/shell.lua",
83 ["wt.path"] = "src/wt/path.lua",
84 ["wt.git"] = "src/wt/git.lua",
85 ["wt.config"] = "src/wt/config.lua",
86 ["wt.hooks"] = "src/wt/hooks.lua",
87 ["wt.help"] = "src/wt/help.lua",
88 ["wt.cmd.clone"] = "src/wt/cmd/clone.lua",
89 ["wt.cmd.new"] = "src/wt/cmd/new.lua",
90 ["wt.cmd.add"] = "src/wt/cmd/add.lua",
91 ["wt.cmd.remove"] = "src/wt/cmd/remove.lua",
92 ["wt.cmd.list"] = "src/wt/cmd/list.lua",
93 ["wt.cmd.fetch"] = "src/wt/cmd/fetch.lua",
94 ["wt.cmd.init"] = "src/wt/cmd/init.lua",
95}
96
97local function main()
98 -- Collect all modules that exist
99 local modules = {}
100 for _, mod_name in ipairs(MODULE_ORDER) do
101 local path = MODULE_PATHS[mod_name]
102 if path and file_exists(path) then
103 local content, err = read_file(path)
104 if content then
105 modules[mod_name] = content
106 else
107 io.stderr:write("warning: failed to read " .. path .. ": " .. (err or "unknown error") .. "\n")
108 end
109 end
110 end
111
112 -- Read main entry point
113 local main_content, err = read_file("src/main.lua")
114 if not main_content then
115 io.stderr:write("error: failed to read src/main.lua: " .. (err or "unknown error") .. "\n")
116 os.exit(1)
117 end
118
119 -- Start output
120 io.write([[#!/usr/bin/env lua
121
122-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
123--
124-- SPDX-License-Identifier: GPL-3.0-or-later
125
126-- AUTO-GENERATED FILE - Do not edit directly
127-- Edit src/*.lua and run 'make dist' to regenerate
128
129]])
130
131 -- If we have modules to embed, set up the custom require system
132 local has_modules = next(modules) ~= nil
133
134 if has_modules then
135 io.write([[-- Embedded module loader
136local _EMBEDDED_MODULES = {}
137local _LOADED_MODULES = {}
138local _real_require = require
139
140local function _embedded_require(name)
141 if _LOADED_MODULES[name] then
142 return _LOADED_MODULES[name]
143 end
144 if _EMBEDDED_MODULES[name] then
145 local loader = load(_EMBEDDED_MODULES[name], name)
146 if loader then
147 local result = loader()
148 _LOADED_MODULES[name] = result or true
149 return _LOADED_MODULES[name]
150 end
151 end
152 return _real_require(name)
153end
154require = _embedded_require
155
156]])
157
158 -- Embed each module
159 for _, mod_name in ipairs(MODULE_ORDER) do
160 local content = modules[mod_name]
161 if content then
162 local open, close, escaped = escape_for_longstring(content)
163 io.write("_EMBEDDED_MODULES[\"" .. mod_name .. "\"] = " .. open .. escaped .. close .. "\n\n")
164 end
165 end
166 end
167
168 -- Write main content (strip shebang if present, we already wrote one)
169 if main_content:sub(1, 2) == "#!" then
170 main_content = main_content:gsub("^#![^\n]*\n", "")
171 end
172
173 -- Strip SPDX header if present (we already wrote one)
174 main_content = main_content:gsub("^%s*%-%-[^\n]*SPDX%-FileCopyrightText[^\n]*\n", "")
175 main_content = main_content:gsub("^%s*%-%-[^\n]*\n", "") -- blank comment
176 main_content = main_content:gsub("^%s*%-%-[^\n]*SPDX%-License%-Identifier[^\n]*\n", "")
177
178 io.write(main_content)
179end
180
181main()