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