squish.lua

1
#!/usr/bin/env lua
2
 
3
-- Initialise LuaRocks if present
4
pcall(require, "luarocks.require");
5
 
6
local short_opts = { v = "verbose", vv = "very_verbose", o = "output", q = "quiet", qq = "very_quiet", g = "debug" }
7
local opts = { use_http = false, module_compat = not not _ENV };
8
 
9
for _, opt in ipairs(arg) do
10
	if opt:match("^%-") then
11
		local name = opt:match("^%-%-?([^%s=]+)()")
12
		name = (short_opts[name] or name):gsub("%-+", "_");
13
		if name:match("^no_") then
14
			name = name:sub(4, -1);
15
			opts[name] = false;
16
		else
17
			opts[name] = opt:match("=(.*)$") or true;
18
		end
19
	else
20
		base_path = opt;
21
	end
22
end
23
 
24
if opts.very_verbose then opts.verbose = true; end
25
if opts.very_quiet then opts.quiet = true; end
26
 
27
local noprint = function () end
28
local print_err, print_info, print_verbose, print_debug = noprint, noprint, noprint, noprint;
29
 
30
if not opts.very_quiet then print_err = print; end
31
if not opts.quiet then print_info = print; end
32
if opts.verbose or opts.very_verbose then print_verbose = print; end
33
if opts.very_verbose then print_debug = print; end
34
 
35
print = print_verbose;
36
 
37
local modules, main_files, resources = {}, {}, {};
38
 
39
--  Functions to be called from squishy file  --
40
 
41
function Module(name)
42
	if modules[name] then
43
		print_verbose("Ignoring duplicate module definition for "..name);
44
		return function () end
45
	end
46
	local i = #modules+1;
47
	modules[i] = { name = name, url = ___fetch_url };
48
	modules[name] = modules[i];
49
	return function (path)
50
		modules[i].path = path;
51
	end
52
end
53
 
54
function Resource(name, path)
55
	local i = #resources+1;
56
	resources[i] = { name = name, path = path or name };
57
	return function (path)
58
		resources[i].path = path;
59
	end
60
end
61
 
62
function AutoFetchURL(url)
63
	___fetch_url = url;
64
end
65
 
66
function Main(fn)
67
	table.insert(main_files, fn);
68
end
69
 
70
function Output(fn)
71
	if opts.output == nil then
72
		out_fn = fn;
73
	end
74
end
75
 
76
function Option(name)
77
	name = name:gsub("%-", "_");
78
	if opts[name] == nil then
79
		opts[name] = true;
80
		return function (value)
81
			opts[name] = value;
82
		end
83
	else
84
		return function () end;
85
	end
86
end
87
 
88
function GetOption(name)
89
	return opts[name:gsub('%-', '_')];
90
end
91
 
92
function Message(message)
93
	if not opts.quiet then
94
		print_info(message);
95
	end
96
end
97
 
98
function Error(message)
99
	if not opts.very_quiet then
100
		print_err(message);
101
	end
102
end
103
 
104
function Exit()
105
	os.exit(1);
106
end
107
-- -- -- -- -- -- -- --- -- -- -- -- -- -- -- --
108
 
109
base_path = (base_path or "."):gsub("/$", "").."/"
110
squishy_file = base_path .. "squishy";
111
out_fn = opts.output;
112
 
113
local ok, err = pcall(dofile, squishy_file);
114
 
115
if not ok then
116
	print_err("Couldn't read squishy file: "..err);
117
	os.exit(1);
118
end
119
 
120
if not out_fn then
121
	print_err("No output file specified by user or squishy file");
122
	os.exit(1);
123
elseif #main_files == 0 and #modules == 0 and #resources == 0 then
124
	print_err("No files, modules or resources. Not going to generate an empty file.");
125
	os.exit(1);
126
end
127
 
128
local fetch = {};
129
function fetch.filesystem(path)
130
	local f, err = io.open(path);
131
	if not f then return false, err; end
132
	
133
	local data = f:read("*a");
134
	f:close();
135
	
136
	return data;
137
end
138
 
139
if opts.use_http then
140
	function fetch.http(url)
141
		local http = require "socket.http";
142
		
143
		local body, status = http.request(url);
144
		if status == 200 then
145
			return body;
146
		end
147
		return false, "HTTP status code: "..tostring(status);
148
	end
149
else
150
	function fetch.http(url)
151
		return false, "Module not found. Re-squish with --use-http option to fetch it from "..url;
152
	end
153
end
154
 
155
print_verbose("Resolving modules...");
156
do
157
	local LUA_DIRSEP = package.config:sub(1,1);
158
	local LUA_PATH_MARK = package.config:sub(5,5);
159
	
160
	local package_path = package.path:gsub("[^;]+", function (path)
161
			if not path:match("^%"..LUA_DIRSEP) then
162
				return base_path..path;
163
			end
164
		end):gsub("/%./", "/");
165
	local package_cpath = package.cpath:gsub("[^;]+", function (path)
166
			if not path:match("^%"..LUA_DIRSEP) then
167
				return base_path..path;
168
			end
169
		end):gsub("/%./", "/");
170
 
171
	function resolve_module(name, path)
172
	        name = name:gsub("%.", LUA_DIRSEP);
173
	        for c in path:gmatch("[^;]+") do
174
	                c = c:gsub("%"..LUA_PATH_MARK, name);
175
	                print_debug("Looking for "..c)
176
	                local f = io.open(c);
177
	                if f then
178
	                	print_debug("Found!");
179
	                        f:close();
180
                        return c;
181
                	end
182
        	end
183
        	return nil; -- not found
184
	end
185
 
186
	for i, module in ipairs(modules) do
187
		if not module.path then
188
			module.path = resolve_module(module.name, package_path);
189
			if not module.path then
190
				print_err("Couldn't resolve module: "..module.name);
191
			else
192
				-- Strip base_path from resolved path
193
				module.path = module.path:gsub("^"..base_path:gsub("%p", "%%%1"), "");
194
			end
195
		end
196
	end
197
end
198
 
199
for _, module in ipairs(modules) do
200
	if not module.path then
201
		print_err("Exiting due to missing modules without a path");
202
		os.exit(1);
203
	end
204
end
205
 
206
if opts.list_files or opts.list_missing_files then
207
	local function write(filename)
208
		if opts.list_missing_files then
209
	                local f = io.open(filename);
210
	                if f then
211
	                	f:close();
212
	                	return;
213
	                end		
214
		end
215
		io.write(filename, "\n");
216
	end
217
	for _, fn in pairs(main_files) do
218
		write(fn);
219
	end
220
	for _, module in ipairs(modules) do
221
		write(module.path);
222
	end
223
	for _, resource in ipairs(resources) do
224
		write(resource.path);
225
	end
226
	return;
227
end
228
 
229
print_info("Writing "..out_fn.."...");
230
local f, err = io.open(out_fn, "w+");
231
if not f then
232
	print_err("Couldn't open output file: "..tostring(err));
233
	os.exit(1);
234
end
235
 
236
if opts.executable then
237
	if opts.executable == true then
238
		f:write("#!/usr/bin/env lua\n");
239
	else
240
		f:write("#!"..opts.executable, "\n");
241
	end
242
end
243
 
244
print_verbose("Packing modules...");
245
for _, module in ipairs(modules) do
246
	local modulename, path = module.name, module.path;
247
	if module.path:sub(1,1) ~= "/" then
248
		path = base_path..module.path;
249
	end
250
	print_debug("Packing "..modulename.." ("..path..")...");
251
	local data, err = fetch.filesystem(path);
252
	if (not data) and module.url then
253
		local url = module.url:gsub("%?", module.path);
254
		print_debug("Fetching: ".. url)
255
		if url:match("^https?://") then
256
			data, err = fetch.http(url);
257
		elseif url:match("^file://") or url:match("^[/%.]") then
258
			local dataf, dataerr = io.open((url:gsub("^file://", "")));
259
			if dataf then
260
				data, err = dataf:read("*a");
261
				dataf:close();
262
			else
263
				data, err = nil, dataerr;
264
			end
265
		end
266
	end
267
	if data then
268
		data = data:gsub("^#[^\r\n]*\r?\n", ""); -- Remove shebang if any (or we can't concat)
269
		if not opts.debug then
270
			f:write("package.preload['", modulename, "'] = (function (...)\n");
271
			if opts.module_compat then
272
				f:write [[
273
					local _ENV = _ENV;
274
					local function module(name, ...)
275
						local t = package.loaded[name] or _ENV[name] or { _NAME = name };
276
						package.loaded[name] = t;
277
						for i = 1, select("#", ...) do
278
							(select(i, ...))(t);
279
						end
280
						_ENV = t;
281
						_M = t;
282
						return t;
283
					end
284
				]];
285
			end
286
			f:write(data);
287
			f:write(" end)\n");
288
		else
289
			f:write("package.preload['", modulename, "'] = assert(loadstring(\n");
290
			f:write(("%q\n"):format(data));
291
			f:write(", ", ("%q"):format("@"..path), "))\n");
292
		end
293
	else
294
		print_err("Couldn't pack module '"..modulename.."': "..(err or "unknown error... path to module file correct?"));
295
		os.exit(1);
296
	end
297
end
298
 
299
if #resources > 0 then
300
	print_verbose("Packing resources...")
301
	f:write("do local resources = {};\n");
302
	for _, resource in ipairs(resources) do
303
		local name, path = resource.name, resource.path;
304
		local res_file, err = io.open(base_path..path, "rb");
305
		if not res_file then
306
			print_err("Couldn't load resource: "..tostring(err));
307
			os.exit(1);
308
		end
309
		local data = res_file:read("*a");
310
		local maxequals = 0;
311
		data:gsub("(=+)", function (equals_string) maxequals = math.max(maxequals, #equals_string); end);
312
		
313
		f:write(("resources[%q] = %q"):format(name, data));
314
--[[		f:write(("resources[%q] = ["):format(name), string.rep("=", maxequals+1), "[");
315
		f:write(data);
316
		f:write("]", string.rep("=", maxequals+1), "];"); ]]
317
	end
318
	if opts.virtual_io then
319
		local vio = require_resource("vio");
320
		if not vio then
321
			print_err("Virtual IO requested but is not enabled in this build of squish");
322
		else
323
			-- Insert vio library
324
			f:write(vio, "\n")
325
			-- Override standard functions to use vio if opening a resource
326
			f:write[[local io_open, io_lines = io.open, io.lines; function io.open(fn, mode)
327
					if not resources[fn] then
328
						return io_open(fn, mode);
329
					else
330
						return vio.open(resources[fn]);
331
				end end
332
				function io.lines(fn)
333
					if not resources[fn] then
334
						return io_lines(fn);
335
					else
336
						return vio.open(resources[fn]):lines()
337
				end end
338
				local _dofile = dofile;
339
				function dofile(fn)
340
					if not resources[fn] then
341
						return _dofile(fn);
342
					else
343
						return assert(loadstring(resources[fn]))();
344
				end end
345
				local _loadfile = loadfile;
346
				function loadfile(fn)
347
					if not resources[fn] then
348
						return _loadfile(fn);
349
					else
350
						return loadstring(resources[fn], "@"..fn);
351
				end end ]]
352
		end
353
	end
354
	f:write[[function require_resource(name) return resources[name] or error("resource '"..tostring(name).."' not found"); end end ]]
355
end
356
 
357
print_debug("Finalising...")
358
for _, fn in pairs(main_files) do
359
	local fin, err = io.open(base_path..fn);
360
	if not fin then
361
		print_err("Failed to open "..fn..": "..err);
362
		os.exit(1);
363
	else
364
		f:write((fin:read("*a"):gsub("^#.-\n", "")));
365
		fin:close();
366
	end
367
end
368
 
369
f:close();
370
 
371
print_info("OK!");