1--
2-- os.lua
3-- Additions to the OS namespace.
4-- Copyright (c) 2002-2014 Jason Perkins and the Premake project
5--
6
7
8---
9-- Extend Lua's built-in os.execute() with token expansion and
10-- path normalization.
11--
12
13	premake.override(os, "execute", function(base, cmd)
14		cmd = os.translateCommands(cmd)
15		return base(cmd)
16	end)
17
18
19
20---
21-- Same as os.execute(), but accepts string formatting arguments.
22---
23
24	function os.executef(cmd, ...)
25		cmd = string.format(cmd, ...)
26		return os.execute(cmd)
27	end
28
29
30
31--
32-- Scan the well-known system locations for a particular library.
33--
34
35	local function parse_ld_so_conf(conf_file)
36		-- Linux ldconfig file parser to find system library locations
37		local first, last
38		local dirs = { }
39		for line in io.lines(conf_file) do
40			-- ignore comments
41			first = line:find("#", 1, true)
42			if first ~= nil then
43				line = line:sub(1, first - 1)
44			end
45
46			if line ~= "" then
47				-- check for include files
48				first, last = line:find("include%s+")
49				if first ~= nil then
50					-- found include glob
51					local include_glob = line:sub(last + 1)
52					local includes = os.matchfiles(include_glob)
53					for _, v in ipairs(includes) do
54						dirs = table.join(dirs, parse_ld_so_conf(v))
55					end
56				else
57					-- found an actual ld path entry
58					table.insert(dirs, line)
59				end
60			end
61		end
62		return dirs
63	end
64
65	local function get_library_search_path()
66		local path
67		if os.istarget("windows") then
68			path = os.getenv("PATH") or ""
69		elseif os.istarget("haiku") then
70			path = os.getenv("LIBRARY_PATH") or ""
71		else
72			if os.istarget("darwin") then
73				path = os.getenv("DYLD_LIBRARY_PATH") or ""
74			else
75				path = os.getenv("LD_LIBRARY_PATH") or ""
76
77				for _, prefix in ipairs({"", "/opt"}) do
78					local conf_file = prefix .. "/etc/ld.so.conf"
79					if os.isfile(conf_file) then
80						for _, v in ipairs(parse_ld_so_conf(conf_file)) do
81							if (#path > 0) then
82								path = path .. ":" .. v
83							else
84								path = v
85							end
86						end
87					end
88				end
89			end
90
91			path = path or ""
92			local archpath = "/lib:/usr/lib:/usr/local/lib"
93			if os.is64bit() and not (os.istarget("darwin")) then
94				archpath = "/lib64:/usr/lib64/:usr/local/lib64" .. ":" .. archpath
95			end
96			if (#path > 0) then
97				path = path .. ":" .. archpath
98			else
99				path = archpath
100			end
101		end
102
103		return path
104	end
105
106	function os.findlib(libname, libdirs)
107		-- libname: library name with or without prefix and suffix
108		-- libdirs: (array or string): A set of additional search paths
109
110		local path = get_library_search_path()
111		local formats
112
113		-- assemble a search path, depending on the platform
114		if os.istarget("windows") then
115			formats = { "%s.dll", "%s" }
116		elseif os.istarget("haiku") then
117			formats = { "lib%s.so", "%s.so" }
118		else
119			if os.istarget("darwin") then
120				formats = { "lib%s.dylib", "%s.dylib" }
121			else
122				formats = { "lib%s.so", "%s.so" }
123			end
124
125			table.insert(formats, "%s")
126		end
127
128		local userpath = ""
129
130		if type(libdirs) == "string" then
131			userpath = libdirs
132		elseif type(libdirs) == "table" then
133			userpath = table.implode(libdirs, "", "", ":")
134		end
135
136		if (#userpath > 0) then
137			if (#path > 0) then
138				path = userpath .. ":" .. path
139			else
140				path = userpath
141			end
142		end
143
144		for _, fmt in ipairs(formats) do
145			local name = string.format(fmt, libname)
146			local result = os.pathsearch(name, path)
147			if result then return result end
148		end
149	end
150
151	function os.findheader(headerpath, headerdirs)
152		-- headerpath: a partial header file path
153		-- headerdirs: additional header search paths
154
155		local path = get_library_search_path()
156
157		-- replace all /lib by /include
158		path = path .. ':'
159		path = path:gsub ('/lib[0-9]*([:/])', '/include%1')
160		path = path:sub (1, #path - 1)
161
162		local userpath = ""
163
164		if type(headerdirs) == "string" then
165			userpath = headerdirs
166		elseif type(headerdirs) == "table" then
167			userpath = table.implode(headerdirs, "", "", ":")
168		end
169
170		if (#userpath > 0) then
171			if (#path > 0) then
172				path = userpath .. ":" .. path
173			else
174				path = userpath
175			end
176		end
177
178		local result = os.pathsearch (headerpath, path)
179		return result
180	end
181
182--
183-- Retrieve the current target operating system ID string.
184--
185
186	function os.target()
187		return _OPTIONS.os or _TARGET_OS
188	end
189
190	function os.get()
191		local caller = filelineinfo(2)
192		premake.warnOnce(caller, "os.get() is deprecated, use 'os.target()' or 'os.host()'.\n   @%s\n", caller)
193		return os.target()
194	end
195
196	-- deprecate _OS
197	_G_metatable = {
198		__index = function(t, k)
199			if (k == '_OS') then
200				premake.warnOnce("_OS+get", "_OS is deprecated, use '_TARGET_OS'.")
201				return rawget(t, "_TARGET_OS")
202			else
203				return rawget(t, k)
204			end
205		end,
206
207		__newindex = function(t, k, v)
208			if (k == '_OS') then
209				premake.warnOnce("_OS+set", "_OS is deprecated, use '_TARGET_OS'.")
210				rawset(t, "_TARGET_OS", v)
211			else
212				rawset(t, k, v)
213			end
214		end
215	}
216	setmetatable(_G, _G_metatable)
217
218
219
220--
221-- Check the current target operating system; may be set with the /os command line flag.
222--
223
224	function os.istarget(id)
225		local tags = os.getSystemTags(os.target())
226		return table.contains(tags, id:lower())
227	end
228
229	function os.is(id)
230		local caller = filelineinfo(2)
231		premake.warnOnce(caller, "os.is() is deprecated, use 'os.istarget()' or 'os.ishost()'.\n   @%s\n", caller)
232		return os.istarget(id)
233	end
234
235
236--
237-- Check the current host operating system.
238--
239
240	function os.ishost(id)
241		local tags = os.getSystemTags(os.host())
242		return table.contains(tags, id:lower())
243	end
244
245
246---
247-- Determine if a directory exists on the file system, and that it is a
248-- directory and not a file.
249--
250-- @param p
251--    The path to check.
252-- @return
253--    True if a directory exists at the given path.
254---
255
256	premake.override(os, "isdir", function(base, p)
257		p = path.normalize(p)
258		return base(p)
259	end)
260
261
262
263---
264-- Determine if a file exists on the file system, and that it is a
265-- file and not a directory.
266--
267-- @param p
268--    The path to check.
269-- @return
270--    True if a file exists at the given path.
271---
272
273	premake.override(os, "isfile", function(base, p)
274		p = path.normalize(p)
275		return base(p)
276	end)
277
278
279
280--
281-- Determine if the current system is running a 64-bit architecture.
282--
283
284	local _is64bit
285
286	local _64BitHostTypes = {
287		"x86_64",
288		"ia64",
289		"amd64",
290		"ppc64",
291		"powerpc64",
292		"sparc64"
293	}
294
295	function os.is64bit()
296		-- This can be expensive to compute, so cache and reuse the response
297		if _is64bit ~= nil then
298			return _is64bit
299		end
300
301		_is64bit = false
302
303		-- Call the native code implementation. If this returns true then
304		-- we're 64-bit, otherwise do more checking locally
305		if (os._is64bit()) then
306			_is64bit = true
307		else
308			-- Identify the system
309			local arch
310			if os.ishost("windows") then
311				arch = os.getenv("PROCESSOR_ARCHITECTURE")
312			elseif os.ishost("macosx") then
313				arch = os.outputof("echo $HOSTTYPE")
314			else
315				arch = os.outputof("uname -m")
316			end
317
318			-- Check our known 64-bit identifiers
319			arch = arch:lower()
320			for _, hosttype in ipairs(_64BitHostTypes) do
321				if arch:find(hosttype) then
322					_is64bit = true
323				end
324			end
325		end
326
327		return _is64bit
328	end
329
330
331---
332-- Perform a wildcard search for files and directories.
333--
334-- @param mask
335--    The file search pattern. Use "*" to match any part of a file or
336--    directory name, "**" to recurse into subdirectories.
337-- @return
338--    A table containing the matched file or directory names.
339---
340
341	function os.match(mask)
342		mask = path.normalize(mask)
343		local starpos = mask:find("%*")
344		local before = path.getdirectory(starpos and mask:sub(1, starpos - 1) or mask)
345		local slashpos = starpos and mask:find("/", starpos)
346		local after = slashpos and mask:sub(slashpos + 1)
347
348		-- Only recurse for path components starting with '**':
349		local recurse = starpos and
350			mask:sub(starpos + 1, starpos + 1) == '*' and
351			(starpos == 1 or mask:sub(starpos - 1, starpos - 1) == '/')
352
353		local results = { }
354
355		if recurse then
356			local submask = mask:sub(1, starpos) .. mask:sub(starpos + 2)
357			results = os.match(submask)
358
359			local pattern = mask:sub(1, starpos)
360			local m = os.matchstart(pattern)
361			while os.matchnext(m) do
362				if not os.matchisfile(m) then
363					local matchpath = path.join(before, os.matchname(m), mask:sub(starpos))
364					results = table.join(results, os.match(matchpath))
365				end
366			end
367			os.matchdone(m)
368		else
369			local pattern = mask:sub(1, slashpos and slashpos - 1)
370			local m = os.matchstart(pattern)
371				while os.matchnext(m) do
372				if not (slashpos and os.matchisfile(m)) then
373					local matchpath = path.join(before, matchpath, os.matchname(m))
374					if after then
375						results = table.join(results, os.match(path.join(matchpath, after)))
376					else
377						table.insert(results, matchpath)
378						end
379					end
380				end
381				os.matchdone(m)
382			end
383
384		return results
385	end
386
387
388---
389-- Perform a wildcard search for directories.
390--
391-- @param mask
392--    The search pattern. Use "*" to match any part of a directory
393--    name, "**" to recurse into subdirectories.
394-- @return
395--    A table containing the matched directory names.
396---
397
398	function os.matchdirs(mask)
399		local results = os.match(mask)
400		for i = #results, 1, -1 do
401			if not os.isdir(results[i]) then
402				table.remove(results, i)
403			end
404		end
405		return results
406	end
407
408
409---
410-- Perform a wildcard search for files.
411--
412-- @param mask
413--    The search pattern. Use "*" to match any part of a file
414--    name, "**" to recurse into subdirectories.
415-- @return
416--    A table containing the matched directory names.
417---
418
419	function os.matchfiles(mask)
420		local results = os.match(mask)
421		for i = #results, 1, -1 do
422			if not os.isfile(results[i]) then
423				table.remove(results, i)
424			end
425		end
426		return results
427	end
428
429--
430-- An overload of the os.mkdir() function, which will create any missing
431-- subdirectories along the path.
432--
433
434	local builtin_mkdir = os.mkdir
435	function os.mkdir(p)
436		p = path.normalize(p)
437
438		local dir = iif(p:startswith("/"), "/", "")
439		for part in p:gmatch("[^/]+") do
440			dir = dir .. part
441
442			if (part ~= "" and not path.isabsolute(part) and not os.isdir(dir)) then
443				local ok, err = builtin_mkdir(dir)
444				if (not ok) then
445					return nil, err
446				end
447			end
448
449			dir = dir .. "/"
450		end
451
452		return true
453	end
454
455
456--
457-- Run a shell command and return the output.
458--
459-- @param cmd Command to execute
460-- @param streams Standard stream(s) to output
461-- 		Must be one of
462--		- "both" (default)
463--		- "output" Return standard output stream content only
464--		- "error" Return standard error stream content only
465--
466
467	function os.outputof(cmd, streams)
468		cmd = path.normalize(cmd)
469		streams = streams or "both"
470		local redirection
471		if streams == "both" then
472			redirection = " 2>&1"
473		elseif streams == "output" then
474			redirection = " 2>/dev/null"
475		elseif streams == "error" then
476			redirection = " 2>&1 1>/dev/null"
477		else
478			error ('Invalid stream(s) selection. "output", "error", or "both" expected.')
479		end
480
481		local pipe = io.popen(cmd .. redirection)
482		local result = pipe:read('*a')
483		local success, what, code = pipe:close()
484		if success then
485			-- chomp trailing newlines
486			if result then
487				result = string.gsub(result, "[\r\n]+$", "")
488			end
489
490			return result, code, what
491		else
492			return nil, code, what
493		end
494	end
495
496
497--
498-- @brief An overloaded os.remove() that will be able to handle list of files,
499--        as well as wildcards for files. Uses the syntax os.matchfiles() for
500--        matching pattern wildcards.
501--
502-- @param f A file, a wildcard, or a list of files or wildcards to be removed
503--
504-- @return true on success, false and an appropriate error message on error
505--
506-- @example     ok, err = os.remove{"**.bak", "**.log"}
507--              if not ok then
508--                  error(err)
509--              end
510--
511
512	local builtin_remove = os.remove
513	function os.remove(f)
514		-- in case of string, just match files
515		if type(f) == "string" then
516			local p = os.matchfiles(f)
517			for _, v in pairs(p) do
518				local ok, err, code = builtin_remove(v)
519				if not ok then
520					return ok, err, code
521				end
522			end
523			if #p == 0 then
524				return nil, "Couldn't find any file matching: " .. f, 1
525			end
526		-- in case of table, match files for every table entry
527		elseif type(f) == "table" then
528			for _, v in pairs(f) do
529				local ok, err, code = os.remove(v)
530				if not ok then
531					return ok, err, code
532				end
533			end
534		end
535
536		return true
537	end
538
539
540--
541-- Remove a directory, along with any contained files or subdirectories.
542--
543-- @return true on success, false and an appropriate error message on error
544
545	local builtin_rmdir = os.rmdir
546	function os.rmdir(p)
547		-- recursively remove subdirectories
548		local dirs = os.matchdirs(p .. "/*")
549		for _, dname in ipairs(dirs) do
550			local ok, err = os.rmdir(dname)
551			if not ok then
552				return ok, err
553			end
554		end
555
556		-- remove any files
557		local files = os.matchfiles(p .. "/*")
558		for _, fname in ipairs(files) do
559			local ok, err = os.remove(fname)
560			if not ok then
561				return ok, err
562			end
563		end
564
565		-- remove this directory
566		return builtin_rmdir(p)
567	end
568
569
570---
571-- Return information about a file.
572---
573
574	premake.override(os, "stat", function(base, p)
575		p = path.normalize(p)
576		return base(p)
577	end)
578
579
580
581---
582-- Translate command tokens into their OS or action specific equivalents.
583---
584
585	os.commandTokens = {
586		_ = {
587			chdir = function(v)
588				return "cd " .. path.normalize(v)
589			end,
590			copy = function(v)
591				return "cp -rf " .. path.normalize(v)
592			end,
593			copyfile = function(v)
594				return "cp -f " .. path.normalize(v)
595			end,
596			copydir = function(v)
597				return "cp -rf " .. path.normalize(v)
598			end,
599			delete = function(v)
600				return "rm -f " .. path.normalize(v)
601			end,
602			echo = function(v)
603				return "echo " .. v
604			end,
605			mkdir = function(v)
606				return "mkdir -p " .. path.normalize(v)
607			end,
608			move = function(v)
609				return "mv -f " .. path.normalize(v)
610			end,
611			rmdir = function(v)
612				return "rm -rf " .. path.normalize(v)
613			end,
614			touch = function(v)
615				return "touch " .. path.normalize(v)
616			end,
617		},
618		windows = {
619			chdir = function(v)
620				return "chdir " .. path.translate(path.normalize(v))
621			end,
622			copy = function(v)
623				v = path.translate(path.normalize(v))
624
625				-- Detect if there's multiple parts to the input, if there is grab the first part else grab the whole thing
626				local src = string.match(v, '^".-"') or string.match(v, '^.- ') or v
627
628				-- Strip the trailing space from the second condition so that we don't have a space between src and '\\NUL'
629				src = string.match(src, '^.*%S')
630
631				return "IF EXIST " .. src .. "\\ (xcopy /Q /E /Y /I " .. v .. " > nul) ELSE (xcopy /Q /Y /I " .. v .. " > nul)"
632			end,
633			copyfile = function(v)
634				v = path.translate(path.normalize(v))
635				-- XCOPY doesn't have a switch to assume destination is a file when it doesn't exist.
636				-- A trailing * will suppress the prompt but requires the file extensions be the same length.
637				-- Just use COPY instead, it actually works.
638				return "copy /B /Y " .. v
639			end,
640			copydir = function(v)
641				v = path.translate(path.normalize(v))
642				return "xcopy /Q /E /Y /I " .. v
643			end,
644			delete = function(v)
645				return "del " .. path.translate(path.normalize(v))
646			end,
647			echo = function(v)
648				return "echo " .. v
649			end,
650			mkdir = function(v)
651				v = path.translate(path.normalize(v))
652				return "IF NOT EXIST " .. v .. " (mkdir " .. v .. ")"
653			end,
654			move = function(v)
655				return "move /Y " .. path.translate(path.normalize(v))
656			end,
657			rmdir = function(v)
658				return "rmdir /S /Q " .. path.translate(path.normalize(v))
659			end,
660			touch = function(v)
661				v = path.translate(path.normalize(v))
662				return string.format("type nul >> %s && copy /b %s+,, %s", v, v, v)
663			end,
664		}
665	}
666
667	function os.translateCommands(cmd, map)
668		map = map or os.target()
669		if type(map) == "string" then
670			map = os.commandTokens[map] or os.commandTokens["_"]
671		end
672
673		local processOne = function(cmd)
674			local i, j, prev
675			repeat
676				i, j = cmd:find("{.-}")
677				if i then
678					if i == prev then
679						break
680					end
681
682					local token = cmd:sub(i + 1, j - 1):lower()
683					local args = cmd:sub(j + 2)
684					local func = map[token] or os.commandTokens["_"][token]
685					if func then
686						cmd = cmd:sub(1, i -1) .. func(args)
687					end
688
689					prev = i
690				end
691			until i == nil
692			return cmd
693		end
694
695		if type(cmd) == "table" then
696			local result = {}
697			for i = 1, #cmd do
698				result[i] = processOne(cmd[i])
699			end
700			return result
701		else
702			return processOne(cmd)
703		end
704	end
705
706
707
708---
709-- Apply os slashes for decorated command paths.
710---
711	function os.translateCommandAndPath(dir, map)
712		if map == 'windows' then
713			return path.translate(dir)
714		end
715		return dir
716	end
717
718---
719-- Translate decorated command paths into their OS equivalents.
720---
721	function os.translateCommandsAndPaths(cmds, basedir, location, map)
722		local translatedBaseDir = path.getrelative(location, basedir)
723
724		map = map or os.target()
725
726		local translateFunction = function(value)
727			local result = path.join(translatedBaseDir, value)
728			result = os.translateCommandAndPath(result, map)
729			if value:endswith('/') or value:endswith('\\') or -- if orginal path ends with a slash then ensure the same
730			   value:endswith('/"') or value:endswith('\\"') then
731				result = result .. '/'
732			end
733			return result
734		end
735
736		local processOne = function(cmd)
737			local replaceFunction = function(value)
738				value = value:sub(3, #value - 1)
739				return '"' .. translateFunction(value) .. '"'
740			end
741			return string.gsub(cmd, "%%%[[^%]\r\n]*%]", replaceFunction)
742		end
743
744		if type(cmds) == "table" then
745			local result = {}
746			for i = 1, #cmds do
747				result[i] = processOne(cmds[i])
748			end
749			return os.translateCommands(result, map)
750		else
751			return os.translateCommands(processOne(cmds), map)
752		end
753	end
754
755
756--
757-- Generate a UUID.
758--
759
760	os._uuids = {}
761
762	local builtin_uuid = os.uuid
763	function os.uuid(name)
764		local id = builtin_uuid(name)
765		if name then
766			if os._uuids[id] and os._uuids[id] ~= name then
767				premake.warnOnce(id, "UUID clash between %s and %s", os._uuids[id], name)
768			end
769			os._uuids[id] = name
770		end
771		return id
772	end
773
774
775--
776-- Get a set of tags for different 'platforms'
777--
778
779	os.systemTags =
780	{
781		["aix"]      = { "aix",     "posix" },
782		["bsd"]      = { "bsd",     "posix" },
783		["haiku"]    = { "haiku",   "posix" },
784		["ios"]      = { "ios",     "darwin", "posix", "mobile" },
785		["linux"]    = { "linux",   "posix" },
786		["macosx"]   = { "macosx",  "darwin", "posix" },
787		["solaris"]  = { "solaris", "posix" },
788		["windows"]  = { "windows", "win32" },
789	}
790
791	function os.getSystemTags(name)
792		return os.systemTags[name:lower()] or { name:lower() }
793	end
794