doodle3d-firmware/extra/scripts/attach-license.lua

264 lines
8.1 KiB
Lua
Executable File

#!/usr/bin/env lua
-- TODO:
-- - cstyle comments with lines in them not starting with a '*' are not detected properly
-- - cstyle comments a la '//' are ignored completely
-- - lua block comments are not recognized
-- - in replaceComments(), source files should only be replaced with temp file if it has actually been changed
local lfs = require('lfs')
LICENSE_COMMENT_PREFIX = "This file is part of the Doodle3D project"
local specs = nil
local headers = {}
local function getFileList(path)
local result = {}
if not path or type(path) ~= 'string' then return result end
for file in lfs.dir(path) do
--local fileWithSubPath = subPath and subPath..'/'..file or file
local subPath = path..'/'..file
if lfs.attributes(subPath,'mode') == 'file' then
table.insert(result, subPath)
elseif lfs.attributes(subPath,'mode')== 'directory' and file ~= '.' and file ~= '..' then
local rr = getFileList(subPath)
for _,v in ipairs(rr) do table.insert(result, v) end
end
end
return result
end
-- @returns indexed table with an entry for each line or nil if file could not be read
local function readFileAsLines(file)
local f,err = io.open(file, "r")
if not f then return f,err end
local result = {}
for l in f:lines() do
table.insert(result, l)
end
return result
end
local function findItemInList(haystack, needle)
for _,v in pairs(haystack) do
if v == needle then return true end
end
return false
end
local function getGitChangedFiles(path)
if not path then path = '' end
os.execute('git update-index -q --refresh')
local f,msg = io.popen('git diff-index --name-only HEAD -- ' .. path, "r")
if not f then return f,msg end
local result = {}
for l in f:lines() do
table.insert(result, l)
end
return result
end
local function initHeaders(headersPath)
-- if not firmwareRootPath then firmwareRootPath = '.' end
-- local headersPath = firmwareRootPath .. '/' .. 'extra/license-headers'
print("Reading headers from: " .. headersPath)
headers['lua'] = readFileAsLines(headersPath .. '/header-lua')
headers['sh'] = readFileAsLines(headersPath .. '/header-sh')
headers['cstyle'] = readFileAsLines(headersPath .. '/header-cstyle')
end
-- writes first through last lines from indexed string array 'lines' to f, with newlines in between
-- if last is nil, everything from first on will be written; if first is nil, it is considered to be line 1
local function emitLines(lines, f, first, last)
if first == nil then first = 1 end
if last == nil then last = #lines end
if last - first < 0 then return end
for i=first, last do f:write(lines[i] .. '\n') end
end
function string:hashbang() return self:find('^#!') and true or false end
function string:empty() return self:find('^[%s]*$') and true or false end
function string:comment(filetype)
if filetype == 'lua' then return self:find('^%-%-.*') and true or false
elseif filetype == 'sh' then return self:find('^#.*') and true or false
elseif filetype == 'cstyle' then return self:find('^%s?/?%*.*') and true or false
end
end
-- @returns false if explicitly excluded, type (as string) if matched or nil otherwise
local function matchFileType(file)
local len = file:len()
for _,pat in ipairs(specs.EXCLUDE_FILES) do
local s,e = file:find(pat)
if s == 1 and e == len then return false end
end
for pat,type in pairs(specs.PROCESS_FILES) do
local s,e = file:find(pat)
if s == 1 and e == len then return type end
end
return nil
end
-- @returns first line nr or false if no comment found
-- @returns last line nr if comment found
local function findComment(lines, filetype)
if not (filetype == 'lua' or filetype == 'sh' or filetype == 'cstyle') then return nil,'unrecognized file type' end
local first, last = false, nil
local justConsumeEmpty = false
for i,l in ipairs(lines) do
if i > 1 and l:hashbang() then
break
elseif l:hashbang() then
--ignore this to simulate a continue
elseif not l:comment(filetype) then
if first and l:empty() then
justConsumeEmpty = true
elseif first then
last = i - 1
break
elseif not l:empty() then
break
end
else
if justConsumeEmpty then
last = i - 1
break
elseif not first then
first = i
end
end
end
return first,last
end
local function detectLicenseComment(lines, filetype, first, last)
local hasText = function(line, filetype)
if filetype == 'lua' then return line:find('^[%s%-]*$') == nil and true or false
elseif filetype == 'sh' then return line:find('^[%s#]*$') == nil and true or false
elseif filetype == 'cstyle' then return line:find('^[%s%*/]*$') == nil and true or false
else return nil
end
end
for i,l in ipairs(lines) do
if i >= first and i <= last and hasText(l, filetype) then
return l:find(LICENSE_COMMENT_PREFIX, 1, true) and true or false
end
end
return false
end
-- replaces line range [first, last] with header for filetype
-- if first and/or last are nil, header is inserted at line 1, or 2 if a hashbang is present
-- returns true on success, nil+msg otherwise
local function replaceComment(filepath, filetype, lines, first, last)
-- local filesEqual = function(file1, file2) return os.execute('cmp -s ' .. file1 .. ' ' .. file2) end--and true or false end
if first == nil or last == nil then first = lines[1]:hashbang() and 2 or 1 end
local tmpFileName = os.tmpname()
f,msg = io.open(tmpFileName, "w+")
if not f then return f, "could not open temporary file '" .. tmpFileName .. "' (" .. msg .. ")" end
emitLines(lines, f, 1, first - 1)
emitLines(headers[filetype], f)
emitLines(lines, f, last and last + 1 or first, nil)
f:close()
-- if not filesEqual(tmpFileName, filepath) then
-- print("actually replacing '" .. filepath .. "' with '" .. tmpFileName .. "' now...")
local rv,msg = os.rename(tmpFileName, filepath)
os.remove(tmpFileName) -- clean up
if not rv then return rv,"could not replace file with '" .. tmpFileName .. "' (" .. msg .. ")" end
-- else
-- print("files are equal, not touching original")
-- end
return true
end
local function processFile(filepath, filetype, lines)
local sLine,eLine = findComment(lines, filetype)
if sLine then
if detectLicenseComment(lines, filetype, sLine, eLine) then
print("Replacing comment in file: '" .. filepath .. "' (type: " .. filetype .. ") head comment: " .. sLine .. "-" .. eLine)
replaceComment(filepath, filetype, lines, sLine, eLine)
else
print("Adding comment to file: '" .. filepath .. "' (type: " .. filetype .. ") unrecognized head comment: " .. sLine .. "-" .. eLine)
replaceComment(filepath, filetype, lines, nil, nil)
end
elseif sLine == false then
print("Adding comment to file: '" .. filepath .. "' (type: " .. filetype .. ") no comment")
replaceComment(filepath, filetype, lines, nil, nil)
else
return nil, "invalid type: '" .. filetype .. "'"
end
return true
end
local function main()
if #arg ~= 1 then
print("Please supply directory containing 'license-spec.lua' as argument")
os.exit(1)
end
local pwd = lfs.currentdir()
local scriptPath = arg[0]:match('^(.*)/')
local headersPath = pwd .. '/' .. scriptPath .. '/../license-headers'
initHeaders(headersPath) -- NOTE: this must be precede the chdir below
print("Working directory: " .. arg[1])
if not lfs.chdir(arg[1]) then
print("error: could not change to directory '" .. arg[1] .. "', exiting")
os.exit(1)
end
specs = require('license-spec')
if not specs.BASE_PATH or specs.BASE_PATH:len() == 0 then specs.BASE_PATH = '.' end
local files = getFileList(specs.BASE_PATH)
local changed = getGitChangedFiles(specs.BASE_PATH)
-- for _,l in ipairs(changed) do print("changed: " .. l) end --TEMP
for _,f in ipairs(files) do
local processType = matchFileType(f)
if processType then
if specs.IGNORE_GIT_CHANGED or not findItemInList(changed, f) then
local lines,err = readFileAsLines(f)
if lines then
local rv,msg = processFile(f, processType, lines)
if not rv then print("error: could not process file '" .. f .. "' (" .. msg .. ")") end
else
print("error: could not open '" .. f .. "' (".. err .. ")")
end
else
print("error: file '" .. f .. "' has uncommitted changes in git, refusing to process")
end
elseif processType == false then
print("Skipping excluded file '" .. f .. "'")
end
end
end
main()