495 lines
14 KiB
Lua
Executable File
495 lines
14 KiB
Lua
Executable File
#!/usr/bin/env lua
|
|
|
|
--[[
|
|
Usage: ./scripts/update-copyright
|
|
|
|
Use `make update-copyright` is recommended without least setup.
|
|
|
|
The COPYRIGHT file should be updated after running this. Changes are not added to git, visual
|
|
review is recommended.
|
|
|
|
Assumes the following command-line utilities are available:
|
|
|
|
* lua
|
|
* mkdir
|
|
* find
|
|
* curl
|
|
* luarocks
|
|
* luasocket (luarocks install luasocket) to parse urls
|
|
|
|
This command creates a temporary "work" folder where it does a lot of temporary work,
|
|
including installing rocks inside said folder.
|
|
|
|
Requires internet connection in order to download luarocks and license files.
|
|
|
|
On Macs, you might need to set up OPENSSL_DIR and EXPAT_DIR.
|
|
|
|
The default for mac is:
|
|
|
|
OPENSSL_DIR=/usr/local/opt/openssl/ EXPAT_DIR=/usr/local/opt/expat ./scripts/update-copyright
|
|
|
|
]]
|
|
|
|
setmetatable(_G, nil)
|
|
|
|
local url = require "socket.url"
|
|
|
|
local fmt = string.format
|
|
|
|
local OPENSSL_DIR = os.getenv("OPENSSL_DIR")
|
|
assert(OPENSSL_DIR, "please set the OPENSSL_DIR env variable (needed for installing luasocket)")
|
|
|
|
local EXPAT_DIR = os.getenv("EXPAT_DIR")
|
|
assert(EXPAT_DIR, "please set the EXPAT_DIR env variable (needed for installing luaexpat)")
|
|
|
|
local work_folder = os.tmpname() .. "-update-copyright"
|
|
|
|
local MIT_LICENSE = [[
|
|
%s
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
of the Software, and to permit persons to whom the Software is furnished to do
|
|
so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
]]
|
|
|
|
-- The script will attempt downloading a more recent version of the license file for these projects
|
|
local HARDCODED_DEPENDENCIES = {
|
|
["OpenResty"] = {
|
|
url = "https://openresty.org",
|
|
repo_url = "https://github.com/openresty/openresty",
|
|
},
|
|
["LuaRocks"] = {
|
|
url = "https://luarocks.org",
|
|
repo_url = "https://github.com/luarocks/luarocks",
|
|
},
|
|
["OpenSSL"] = {
|
|
url = "https://github.com/openssl/openssl",
|
|
},
|
|
-- go-pdk dependencies:
|
|
["go-codec"] = {
|
|
url = "https://github.com/ugorji/go",
|
|
},
|
|
["testify"] = {
|
|
url = "https://github.com/stretchr/testify",
|
|
},
|
|
["go-difflib"] = {
|
|
url = "https://github.com/pmezard/go-difflib",
|
|
},
|
|
["go-spew"] = {
|
|
url = "https://github.com/davecgh/go-spew",
|
|
},
|
|
["luasyslog"] = {
|
|
url = "https://github.com/lunarmodules/luasyslog"
|
|
}
|
|
}
|
|
|
|
-- rocks whose license text cannot be easily found
|
|
-- i.e. they don't have a github repo, or don't have a LICENSE file in the repo root,
|
|
-- or don't have a README markdown file from which to parse the license
|
|
local HARDCODED_ROCK_LICENSES = {
|
|
["LPeg"] = {
|
|
url = "http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html#license",
|
|
text = MIT_LICENSE:format("Copyright © 2007-2019 Lua.org, PUC-Rio."),
|
|
},
|
|
["lrandom"] = {
|
|
url = "http://webserver2.tecgraf.puc-rio.br/~lhf/ftp/lua/install.html#license",
|
|
text = MIT_LICENSE:format("Copyright (C) 2018 Luiz Henrique de Figueiredo"),
|
|
},
|
|
["lua-MessagePack"] = {
|
|
url = "https://fperrad.frama.io/lua-MessagePack/#copyright-and-license",
|
|
text = MIT_LICENSE:format("Copyright © 2012-2019 François Perrad"),
|
|
},
|
|
["LuaSocket"] = {
|
|
url = "https://github.com/diegonehab/luasocket/blob/master/LICENSE",
|
|
text = MIT_LICENSE:format("Copyright © 2004-2013 Diego Nehab"),
|
|
},
|
|
["mimetypes"] = {
|
|
url = "https://bitbucket.org/leafstorm/lua-mimetypes/src/default/LICENSE",
|
|
text = MIT_LICENSE:format('Copyright (c) 2011 Matthew "LeafStorm" Frazier') ..
|
|
[[
|
|
======
|
|
|
|
In addition, the MIME types contained in the Software were
|
|
originally obtained from the Python 2.7.1 ``mimetypes.py`` module,
|
|
though they have been considerably modified and augmented.
|
|
Said file was made available under the Python Software Foundation
|
|
license (http://python.org/psf/license/).
|
|
]]
|
|
,
|
|
},
|
|
}
|
|
|
|
-- Try these to get the license file from a github repo:
|
|
local LICENSE_ATTEMPTS = {
|
|
"LICENSE",
|
|
"LICENSE.txt",
|
|
"LICENSE.md",
|
|
"MIT-LICENSE.txt",
|
|
"COPYRIGHT",
|
|
"COPYING",
|
|
}
|
|
|
|
-- Try to get the readme from a github repo using:
|
|
local README_ATTEMPTS = {
|
|
"README.md",
|
|
"README.markdown",
|
|
}
|
|
|
|
-- Try to find the license text inside a readme markdown using these header lines:
|
|
local README_LICENSE_HEADERS = {
|
|
"license",
|
|
"copyright and license",
|
|
"license %(mit%)",
|
|
}
|
|
|
|
|
|
local function write_dot()
|
|
local out = io.output()
|
|
out:write(".")
|
|
out:flush()
|
|
end
|
|
|
|
|
|
-- "a string. With stuff!" -> "a-string-with-stuff"
|
|
local function to_anchor(str)
|
|
return str:lower():gsub("%W+", "-"):gsub("^%-", ""):gsub("%-$", "")
|
|
end
|
|
|
|
|
|
-- returns nil for non-github urls. Returns a "cleaned up" url for github urls
|
|
local function get_github_repo_url(repo_url)
|
|
local parsed_url = assert(url.parse(repo_url))
|
|
|
|
if parsed_url.host ~= "github.com" then
|
|
return nil
|
|
end
|
|
parsed_url.scheme = "https"
|
|
|
|
local without_dotgit = parsed_url.path:match("(.*).git$")
|
|
if without_dotgit then
|
|
parsed_url.path = without_dotgit
|
|
end
|
|
|
|
local without_archive_tgz = parsed_url.path:match("(.*)/archive/.*.tar.gz$")
|
|
if without_archive_tgz then
|
|
parsed_url.path = without_archive_tgz
|
|
end
|
|
|
|
local without_wiki = parsed_url.path:match("(.*)/wiki$")
|
|
if without_wiki then
|
|
parsed_url.path = without_wiki
|
|
end
|
|
|
|
return url.build(parsed_url)
|
|
end
|
|
|
|
|
|
-- returns the homepage, or the github repo url if none provided in the rock
|
|
local function get_rock_homepage(rock)
|
|
if rock.description and rock.description.homepage then
|
|
return rock.description.homepage
|
|
end
|
|
|
|
if rock.source and rock.source.url then
|
|
local gh_url = get_github_repo_url(rock.source.url)
|
|
if gh_url then
|
|
return gh_url
|
|
end
|
|
end
|
|
|
|
error("could not find the homepage for " .. rock.package)
|
|
end
|
|
|
|
|
|
-- Downloads a file using curl. Returns the contents of the file if found, nil otherwise
|
|
local function download_file(file_url)
|
|
local filepath = work_folder .. "/download.tmp"
|
|
assert(os.execute("rm -rf " .. filepath))
|
|
if os.execute(fmt("curl --fail --silent %s > %s", file_url, filepath)) then
|
|
local f = io.open(filepath, "r")
|
|
local text = f:read("*a")
|
|
f:close()
|
|
return text
|
|
end
|
|
end
|
|
|
|
|
|
-- Finds the start of a license section inside a markdown file.
|
|
--
|
|
-- If the line is the start of a license section, it is returned,
|
|
-- plus an offset (more about this in a second).
|
|
--
|
|
-- If the line is not the start of a license section, nil is returned.
|
|
--
|
|
-- The offset is 1 for a 1-liner header or 2 for a 2-liner header.
|
|
--
|
|
-- ## This is a 1-liner header
|
|
--
|
|
-- This is a 2-liner header (it is followed by a line of - or =)
|
|
-- ==============================================================
|
|
local function detect_markdown_license_start(line, next_line)
|
|
local low_line = line:lower()
|
|
for _, header in ipairs(README_LICENSE_HEADERS) do
|
|
if low_line:match("^#+ " .. header .. "$") then
|
|
return line, 1
|
|
end
|
|
|
|
if low_line == header
|
|
and next_line
|
|
and (next_line:match("^=+$") or next_line:match("^%-+$"))
|
|
then
|
|
return line, 2
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- returns truthy if the passed line looks like the end of a license section in a markdown file
|
|
-- * A new markdown header is found (with 1-line or 2-line headers)
|
|
-- * The string [Back to TOC](#table-of-contents) is found
|
|
-- * Non-inline links are found (these can be put at the end of the file, after the license)
|
|
local function is_markdown_license_end(line, next_line)
|
|
return line:match("^#+ .+$") -- markdown 1-line header found
|
|
or line == "[Back to TOC](#table-of-contents)"
|
|
or line:match("^ *%[.+%]: .+$") -- non-inline links
|
|
or next_line
|
|
and (next_line:match("^=+$") or next_line:match("^%-+$")) -- markdown 2-liner header
|
|
end
|
|
|
|
-- Given some markdown text, find the markdown section where the license is, and return:
|
|
-- On the first returned value, the header of the license section
|
|
-- On the second returned value, the text of the license section
|
|
-- Or nil if no license is found
|
|
local function extract_license_from_markdown(markdown)
|
|
local lines = {}
|
|
for line in markdown:gmatch("([^\r\n]*)[\r\n]?") do
|
|
lines[#lines + 1] = line
|
|
end
|
|
|
|
local license_lines = {}
|
|
local license_header, offset
|
|
|
|
local i = 1
|
|
while i < #lines do
|
|
local line, next_line = lines[i], lines[i + 1]
|
|
if license_header then
|
|
-- We are reading the license.
|
|
|
|
if is_markdown_license_end(line, next_line) then
|
|
-- check if we have reached an end
|
|
return license_header, table.concat(license_lines, "\n")
|
|
end
|
|
|
|
-- if no end reached, attach current line
|
|
license_lines[#license_lines + 1] = line
|
|
else
|
|
license_header, offset = detect_markdown_license_start(line, next_line)
|
|
if license_header then
|
|
i = i + offset
|
|
end
|
|
end
|
|
|
|
i = i + 1
|
|
end
|
|
|
|
-- we are reading the license and reached the end of the file. concat and send
|
|
if license_header then
|
|
return license_header, table.concat(license_lines, "\n")
|
|
end
|
|
end
|
|
|
|
|
|
local function find_and_download_license(main_url, alt_url)
|
|
local gh_url = get_github_repo_url(main_url)
|
|
if not gh_url and alt_url then
|
|
gh_url = get_github_repo_url(alt_url)
|
|
end
|
|
|
|
if not gh_url then
|
|
error(fmt("Could not find github repo for: %s / %s", main_url, alt_url))
|
|
end
|
|
|
|
local parsed_url = url.parse(gh_url)
|
|
local user, reponame = parsed_url.path:match("^/(.*)/(.*)")
|
|
if user then
|
|
parsed_url.scheme = "https"
|
|
parsed_url.host = "raw.githubusercontent.com"
|
|
for _, attempt in ipairs(LICENSE_ATTEMPTS) do
|
|
parsed_url.path = fmt("/%s/%s/master/%s", user, reponame, attempt)
|
|
|
|
local attempt_url = url.build(parsed_url)
|
|
local text = download_file(attempt_url)
|
|
if text and #text > 0 then
|
|
parsed_url.host = "github.com"
|
|
parsed_url.path = fmt("/%s/%s/blob/master/%s", user, reponame, attempt)
|
|
local url_for_humans = url.build(parsed_url)
|
|
return url_for_humans, text
|
|
end
|
|
end
|
|
|
|
for _, readme_attempt in ipairs(README_ATTEMPTS) do
|
|
parsed_url.path = fmt("/%s/%s/master/%s", user, reponame, readme_attempt)
|
|
local readme_url = url.build(parsed_url)
|
|
local readme_markdown = download_file(readme_url)
|
|
if readme_markdown then
|
|
local header, text = extract_license_from_markdown(readme_markdown)
|
|
if header and #header > 0 then
|
|
parsed_url.host = "github.com"
|
|
parsed_url.path = fmt("/%s/%s", user, reponame)
|
|
parsed_url.fragment = to_anchor(header)
|
|
local url_for_humans = url.build(parsed_url)
|
|
return url_for_humans, text
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
print("Could not find license file for " .. gh_url)
|
|
return "undefined", "undefined"
|
|
end
|
|
|
|
|
|
-----
|
|
|
|
assert(os.execute(fmt("mkdir -p %s", work_folder)))
|
|
print("Work folder is " .. work_folder)
|
|
|
|
print("Finding and downloading license texts from non-rock dependencies")
|
|
local licenses = {}
|
|
|
|
for name, dep in pairs(HARDCODED_DEPENDENCIES) do
|
|
local license_url, license_text = find_and_download_license(dep.url, dep.repo_url)
|
|
licenses[#licenses + 1] = {
|
|
library = name,
|
|
library_url = dep.url,
|
|
url = license_url,
|
|
text = license_text,
|
|
}
|
|
write_dot()
|
|
end
|
|
print("")
|
|
|
|
print(fmt("Installing rocks in work folder. (Install log: %s/luarocks.log) ...", work_folder))
|
|
|
|
assert(os.execute(fmt("cp kong*.rockspec %s", work_folder)))
|
|
assert(os.execute(fmt("luarocks --lua-version=5.1 --tree %s make %s/kong*.rockspec OPENSSL_DIR=%s EXPAT_DIR=%s 2>&1 > %s/luarocks.log",
|
|
work_folder, work_folder, OPENSSL_DIR, EXPAT_DIR, work_folder)))
|
|
|
|
local rocklist_path = fmt("%s/rocklist.txt", work_folder)
|
|
assert(os.execute(fmt("find %s/lib | grep rockspec > %s", work_folder, rocklist_path)))
|
|
|
|
print("Parsing rockfiles ...")
|
|
|
|
local rocklist = io.open(rocklist_path, "r")
|
|
local rocks = {}
|
|
for rockpath in assert(rocklist:lines()) do
|
|
local rockfile = assert(io.open(rockpath, "r"))
|
|
local rocktext = assert(rockfile:read("*a"))
|
|
rockfile:close()
|
|
|
|
-- parse the text of the rockspec and fill up the `rock` variable
|
|
local rock = {}
|
|
local rockchunk
|
|
if _G.loadstring then
|
|
rockchunk = assert(loadstring(rocktext))
|
|
setfenv(rockchunk, rock)
|
|
else
|
|
rockchunk = assert(load(rocktext, "sandbox string", "bt", rock))
|
|
end
|
|
assert(pcall(rockchunk))
|
|
if rock.package ~= "kong" then -- skip kong itself
|
|
rocks[#rocks + 1] = rock
|
|
end
|
|
end
|
|
rocklist:close()
|
|
|
|
-- sort alphabetically by package
|
|
table.sort(rocks, function(a, b) return a.package:lower() < b.package:lower() end)
|
|
|
|
|
|
print("Searching and downloading license texts from rock repos")
|
|
for _, rock in ipairs(rocks) do
|
|
-- if it was in HARDCODED_DEPENDENCIES, it is already in licenses at this point
|
|
if not HARDCODED_DEPENDENCIES[rock.package] then
|
|
local homepage = get_rock_homepage(rock)
|
|
local license_url, license_text
|
|
local hardcoded = HARDCODED_ROCK_LICENSES[rock.package]
|
|
if hardcoded then
|
|
license_url, license_text = hardcoded.url, hardcoded.text
|
|
else
|
|
local source_url = rock.source and rock.source.url or nil
|
|
license_url, license_text = find_and_download_license(homepage, source_url)
|
|
end
|
|
|
|
licenses[#licenses + 1] = {
|
|
library = rock.package,
|
|
library_url = homepage,
|
|
url = license_url,
|
|
text = license_text,
|
|
}
|
|
end
|
|
|
|
write_dot()
|
|
end
|
|
print("")
|
|
|
|
|
|
print("Writing licenses into COPYRIGHT file")
|
|
|
|
-- sort alphabetically by library name
|
|
table.sort(licenses, function(a, b) return a.library:lower() < b.library:lower() end)
|
|
|
|
local cf = io.open("COPYRIGHT", "w")
|
|
|
|
cf:write[[
|
|
%%%%%%%%%
|
|
|
|
Library
|
|
|
|
Library URL
|
|
License URL
|
|
|
|
License text
|
|
|
|
|
|
]]
|
|
|
|
local LICENSE_FORMAT = [[
|
|
%%%%%%%%%%%%%%%%%%
|
|
|
|
%s
|
|
|
|
%s
|
|
%s
|
|
|
|
%s
|
|
|
|
]]
|
|
for _, license in ipairs(licenses) do
|
|
cf:write(fmt(LICENSE_FORMAT,
|
|
license.library,
|
|
license.library_url,
|
|
license.url,
|
|
license.text))
|
|
end
|
|
|
|
cf:close()
|
|
|
|
print("Cleaning up " .. work_folder)
|
|
assert(os.execute("rm -rf " .. work_folder))
|
|
|
|
print("All done")
|