#!/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()