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