Files
2026-07-13 12:32:21 +08:00

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")