bundle.lua

  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()