Initial community commit
This commit is contained in:
parent
537bcbc862
commit
fc06254474
16440 changed files with 4239995 additions and 2 deletions
45
Src/external_dependencies/openmpt-trunk/include/premake/scripts/RELEASE.md
vendored
Normal file
45
Src/external_dependencies/openmpt-trunk/include/premake/scripts/RELEASE.md
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
# PREMAKE RELEASE CHECKLIST
|
||||
|
||||
## PREP
|
||||
|
||||
* Create a new release branch `release/v5.0-beta1`
|
||||
|
||||
* Update `CHANGES.txt`
|
||||
* Set version at top of file
|
||||
* `premake5 --file=scripts/changes.lua --since=<last_release_rev> changes`
|
||||
* Review and clean up as needed
|
||||
|
||||
* Update `README.md`
|
||||
* "Commits since last release" badge (once out of prerelease replace `v5.0.0-alphaXX` with `latest`)
|
||||
|
||||
* Update version in `src/host/premake.h`
|
||||
|
||||
* Update version in `website/src/pages/download.js`
|
||||
|
||||
* Commit changes and push release branch; wait for CI to pass
|
||||
|
||||
* Prep release announcement from change log
|
||||
|
||||
## RELEASE
|
||||
|
||||
* Run `premake5 package <release branch name> source` (from Posix ideally)
|
||||
|
||||
* On each platform, run `premake5 package <release branch name> binary`
|
||||
|
||||
* Submit Windows binary to [Microsoft malware analysis](https://www.microsoft.com/en-us/wdsi/filesubmission/)
|
||||
|
||||
* Push any remaining changes; tag release branch
|
||||
|
||||
* Create new release on GitHub from `CHANGES.txt`; upload files
|
||||
|
||||
* Post announcement to `@premakeapp`
|
||||
|
||||
## CYCLE
|
||||
|
||||
* Update version in `src/host/premake.h` (e.x `"5.0.0-dev"`)
|
||||
|
||||
* Commit
|
||||
|
||||
* Merge release branch to master
|
||||
|
||||
* Delete release branch
|
69
Src/external_dependencies/openmpt-trunk/include/premake/scripts/changes.lua
vendored
Normal file
69
Src/external_dependencies/openmpt-trunk/include/premake/scripts/changes.lua
vendored
Normal file
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
-- Output a list of merged PRs since last release in the CHANGES.txt format.
|
||||
---
|
||||
|
||||
local usage = "Usage: premake5 --file=scripts/changes.lua --since=<rev> changes"
|
||||
|
||||
local sinceRev = _OPTIONS["since"]
|
||||
|
||||
if not sinceRev then
|
||||
print(usage)
|
||||
error("Missing `--since`", 0)
|
||||
end
|
||||
|
||||
|
||||
local function parsePullRequestId(line)
|
||||
return line:match("#%d+%s")
|
||||
end
|
||||
|
||||
local function parseTitle(line)
|
||||
return line:match("||(.+)")
|
||||
end
|
||||
|
||||
local function parseAuthor(line)
|
||||
return line:match("%s([^%s]-)/")
|
||||
end
|
||||
|
||||
local function parseLog(line)
|
||||
local pr = parsePullRequestId(line)
|
||||
local title = parseTitle(line)
|
||||
local author = parseAuthor(line)
|
||||
return string.format("* PR %s %s (@%s)", pr, title, author)
|
||||
end
|
||||
|
||||
|
||||
local function gatherChanges()
|
||||
local cmd = string.format('git log HEAD "^%s" --merges --first-parent --format="%%s||%%b"', _OPTIONS["since"])
|
||||
local output = os.outputof(cmd)
|
||||
|
||||
changes = {}
|
||||
|
||||
for line in output:gmatch("[^\r\n]+") do
|
||||
table.insert(changes, parseLog(line))
|
||||
end
|
||||
|
||||
return changes
|
||||
end
|
||||
|
||||
|
||||
local function generateChanges()
|
||||
local changes = gatherChanges()
|
||||
table.sort(changes)
|
||||
for i = 1, #changes do
|
||||
print(changes[i])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
newaction {
|
||||
trigger = "changes",
|
||||
description = "Generate list of git merges in CHANGES.txt format",
|
||||
execute = generateChanges
|
||||
}
|
||||
|
||||
newoption {
|
||||
trigger = "since",
|
||||
value = "revision",
|
||||
description = "Log merges since this revision"
|
||||
}
|
||||
|
14
Src/external_dependencies/openmpt-trunk/include/premake/scripts/docscheck.lua
vendored
Normal file
14
Src/external_dependencies/openmpt-trunk/include/premake/scripts/docscheck.lua
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
-- Validate documentation for Premkake APIs.
|
||||
---
|
||||
|
||||
local count = 0
|
||||
for k,v in pairs(premake.field._loweredList) do
|
||||
local docfilepath = "../website/docs/" .. k .. ".md"
|
||||
local exists = os.isfile(docfilepath)
|
||||
if not exists then
|
||||
print("Missing documentation file for: ", k)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
os.exit(count)
|
210
Src/external_dependencies/openmpt-trunk/include/premake/scripts/embed.lua
vendored
Normal file
210
Src/external_dependencies/openmpt-trunk/include/premake/scripts/embed.lua
vendored
Normal file
|
@ -0,0 +1,210 @@
|
|||
--
|
||||
-- Embed the Lua scripts into src/host/scripts.c as static data buffers.
|
||||
-- Embeds minified versions of the actual scripts by default, rather than
|
||||
-- bytecode, as bytecodes are not portable to different architectures. Use
|
||||
-- the `--bytecode` flag to override.
|
||||
--
|
||||
|
||||
local scriptCount = 0
|
||||
|
||||
local function loadScript(fname)
|
||||
fname = path.getabsolute(fname)
|
||||
local f = io.open(fname, "rb")
|
||||
local s = assert(f:read("*all"))
|
||||
f:close()
|
||||
return s
|
||||
end
|
||||
|
||||
|
||||
local function stripScript(s)
|
||||
-- strip tabs
|
||||
local result = s:gsub("[\t]", "")
|
||||
|
||||
-- strip any CRs
|
||||
result = result:gsub("[\r]", "")
|
||||
|
||||
-- strip out block comments
|
||||
result = result:gsub("[^\"']%-%-%[%[.-%]%]", "")
|
||||
result = result:gsub("[^\"']%-%-%[=%[.-%]=%]", "")
|
||||
result = result:gsub("[^\"']%-%-%[==%[.-%]==%]", "")
|
||||
|
||||
-- strip out inline comments
|
||||
result = result:gsub("\n%-%-[^\n]*", "\n")
|
||||
|
||||
-- strip duplicate line feeds
|
||||
result = result:gsub("\n+", "\n")
|
||||
|
||||
-- strip out leading comments
|
||||
result = result:gsub("^%-%-[^\n]*\n", "")
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
local function outputScript(result, script)
|
||||
local data = script.data
|
||||
local length = #data
|
||||
|
||||
if length > 0 then
|
||||
script.table = string.format("builtin_script_%d", scriptCount)
|
||||
scriptCount = scriptCount + 1
|
||||
|
||||
buffered.writeln(result, "// ".. script.name)
|
||||
buffered.writeln(result, "static const unsigned char " .. script.table .. "[] = {")
|
||||
|
||||
for i = 1, length do
|
||||
buffered.write(result, string.format("%3d, ", data:byte(i)))
|
||||
if (i % 32 == 0) then
|
||||
buffered.writeln(result)
|
||||
end
|
||||
end
|
||||
|
||||
buffered.writeln(result, "};")
|
||||
buffered.writeln(result)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function addScript(result, filename, name, data)
|
||||
if not data then
|
||||
if not path.hasextension(filename, ".lua") then
|
||||
data = loadScript(filename)
|
||||
elseif _OPTIONS["bytecode"] then
|
||||
verbosef("Compiling... " .. filename)
|
||||
local output = path.replaceextension(filename, ".luac")
|
||||
local res, err = os.compile(filename, output);
|
||||
if res ~= nil then
|
||||
data = loadScript(output)
|
||||
os.remove(output)
|
||||
else
|
||||
print(err)
|
||||
print("Embedding source instead.")
|
||||
data = stripScript(loadScript(filename))
|
||||
end
|
||||
else
|
||||
data = stripScript(loadScript(filename))
|
||||
end
|
||||
end
|
||||
|
||||
local script = {}
|
||||
script.filename = filename
|
||||
script.name = name
|
||||
script.data = data
|
||||
table.insert(result, script)
|
||||
end
|
||||
|
||||
|
||||
-- Prepare the file header
|
||||
|
||||
local result = buffered.new()
|
||||
buffered.writeln(result, "/* Premake's Lua scripts, as static data buffers for release mode builds */")
|
||||
buffered.writeln(result, "/* DO NOT EDIT - this file is autogenerated - see BUILD.txt */")
|
||||
buffered.writeln(result, "/* To regenerate this file, run: premake5 embed */")
|
||||
buffered.writeln(result, "")
|
||||
buffered.writeln(result, '#include "host/premake.h"')
|
||||
buffered.writeln(result, "")
|
||||
|
||||
-- Find all of the _manifest.lua files within the project
|
||||
|
||||
local mask = path.join(_MAIN_SCRIPT_DIR, "**/_manifest.lua")
|
||||
local manifests = os.matchfiles(mask)
|
||||
|
||||
-- Find all of the _user_modules.lua files within the project
|
||||
|
||||
local userModuleFiles = {}
|
||||
userModuleFiles = table.join(userModuleFiles, os.matchfiles(path.join(_MAIN_SCRIPT_DIR, "**/_user_modules.lua")))
|
||||
userModuleFiles = table.join(userModuleFiles, os.matchfiles(path.join(_MAIN_SCRIPT_DIR, "_user_modules.lua")))
|
||||
|
||||
|
||||
-- Generate table of embedded content.
|
||||
local contentTable = {}
|
||||
local nativeTable = {}
|
||||
|
||||
print("Compiling... ")
|
||||
for mi = 1, #manifests do
|
||||
local manifestName = manifests[mi]
|
||||
local manifestDir = path.getdirectory(manifestName)
|
||||
local moduleName = path.getbasename(manifestDir)
|
||||
local baseDir = path.getdirectory(manifestDir)
|
||||
|
||||
local files = dofile(manifests[mi])
|
||||
for fi = 1, #files do
|
||||
local filename = path.join(manifestDir, files[fi])
|
||||
addScript(contentTable, filename, path.getrelative(baseDir, filename))
|
||||
end
|
||||
|
||||
-- find native code in modules.
|
||||
if moduleName ~= "src" then
|
||||
local nativeFile = path.join(manifestDir, 'native', moduleName .. '.c')
|
||||
if os.isfile(nativeFile) then
|
||||
local pretty_name = moduleName:gsub("^%l", string.upper)
|
||||
table.insert(nativeTable, pretty_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
addScript(contentTable, path.join(_SCRIPT_DIR, "../src/_premake_main.lua"), "src/_premake_main.lua")
|
||||
addScript(contentTable, path.join(_SCRIPT_DIR, "../src/_manifest.lua"), "src/_manifest.lua")
|
||||
|
||||
-- Add the list of modules
|
||||
|
||||
local modules = dofile("../src/_modules.lua")
|
||||
for _, userModules in ipairs(userModuleFiles) do
|
||||
modules = table.join(modules, dofile(userModules))
|
||||
end
|
||||
|
||||
addScript(contentTable, "_modules.lua", "src/_modules.lua", "return {" .. table.implode(modules, '"', '"', ', ') .. "}")
|
||||
|
||||
-- Embed the actual script contents
|
||||
|
||||
print("Embedding...")
|
||||
for mi = 1, #contentTable do
|
||||
outputScript(result, contentTable[mi])
|
||||
end
|
||||
|
||||
-- Generate an index of the script file names. Script names are stored
|
||||
-- relative to the directory containing the manifest, i.e. the main
|
||||
-- Xcode script, which is at $/modules/xcode/xcode.lua is stored as
|
||||
-- "xcode/xcode.lua".
|
||||
buffered.writeln(result, "const buildin_mapping builtin_scripts[] = {")
|
||||
|
||||
for mi = 1, #contentTable do
|
||||
if contentTable[mi].table then
|
||||
buffered.writeln(result, string.format('\t{"%s", %s, sizeof(%s)},', contentTable[mi].name, contentTable[mi].table, contentTable[mi].table))
|
||||
else
|
||||
buffered.writeln(result, string.format('\t{"%s", NULL, 0},', contentTable[mi].name))
|
||||
end
|
||||
end
|
||||
|
||||
buffered.writeln(result, "\t{NULL, NULL, 0}")
|
||||
buffered.writeln(result, "};")
|
||||
buffered.writeln(result, "")
|
||||
|
||||
-- write out the registerModules method.
|
||||
|
||||
for _, name in ipairs(nativeTable) do
|
||||
buffered.writeln(result, string.format("extern void register%s(lua_State* L);", name))
|
||||
end
|
||||
buffered.writeln(result, "")
|
||||
buffered.writeln(result, "void registerModules(lua_State* L)")
|
||||
buffered.writeln(result, "{")
|
||||
buffered.writeln(result, "\t(void)(L);")
|
||||
for _, name in ipairs(nativeTable) do
|
||||
buffered.writeln(result, string.format("\tregister%s(L);", name))
|
||||
end
|
||||
buffered.writeln(result, "}")
|
||||
buffered.writeln(result, "")
|
||||
|
||||
-- Write it all out. Check against the current contents of scripts.c first,
|
||||
-- and only overwrite it if there are actual changes.
|
||||
|
||||
print("Writing...")
|
||||
local scriptsFile = path.getabsolute(path.join(_SCRIPT_DIR, "../src/scripts.c"))
|
||||
local output = buffered.tostring(result)
|
||||
|
||||
local f, err = os.writefile_ifnotequal(output, scriptsFile);
|
||||
if (f < 0) then
|
||||
error(err, 0)
|
||||
elseif (f > 0) then
|
||||
printf("Generated %s...", path.getrelative(os.getcwd(), scriptsFile))
|
||||
end
|
293
Src/external_dependencies/openmpt-trunk/include/premake/scripts/package.lua
vendored
Normal file
293
Src/external_dependencies/openmpt-trunk/include/premake/scripts/package.lua
vendored
Normal file
|
@ -0,0 +1,293 @@
|
|||
---
|
||||
-- Create a source or binary release package.
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
-- Helper function: run a command while hiding its output.
|
||||
---
|
||||
|
||||
local function execQuiet(cmd, ...)
|
||||
cmd = string.format(cmd, ...) .. " > _output_.log 2> _error_.log"
|
||||
local z = os.execute(cmd)
|
||||
os.remove("_output_.log")
|
||||
os.remove("_error_.log")
|
||||
return z
|
||||
end
|
||||
|
||||
|
||||
---
|
||||
-- Check the command line arguments, and show some help if needed.
|
||||
---
|
||||
|
||||
local allowedCompilers = {}
|
||||
|
||||
if os.ishost("windows") then
|
||||
allowedCompilers = {
|
||||
"vs2019",
|
||||
"vs2017",
|
||||
"vs2015",
|
||||
"vs2013",
|
||||
"vs2012",
|
||||
"vs2010",
|
||||
"vs2008",
|
||||
"vs2005",
|
||||
}
|
||||
elseif os.ishost("linux") or os.ishost("bsd") then
|
||||
allowedCompilers = {
|
||||
"gcc",
|
||||
"clang",
|
||||
}
|
||||
elseif os.ishost("macosx") then
|
||||
allowedCompilers = {
|
||||
"clang",
|
||||
}
|
||||
else
|
||||
error("Unsupported host os", 0)
|
||||
end
|
||||
|
||||
local usage = 'usage is: package <branch> <type> [<compiler>]\n' ..
|
||||
' <branch> is the name of the release branch to target\n' ..
|
||||
' <type> is one of "source" or "binary"\n' ..
|
||||
' <compiler> (default: ' .. allowedCompilers[1] .. ') is one of ' .. table.implode(allowedCompilers, "", "", " ")
|
||||
|
||||
if #_ARGS ~= 2 and #_ARGS ~= 3 then
|
||||
error(usage, 0)
|
||||
end
|
||||
|
||||
local branch = _ARGS[1]
|
||||
local kind = _ARGS[2]
|
||||
local compiler = _ARGS[3] or allowedCompilers[1]
|
||||
|
||||
if kind ~= "source" and kind ~= "binary" then
|
||||
print("Invalid package kind: "..kind)
|
||||
error(usage, 0)
|
||||
end
|
||||
|
||||
if not table.contains(allowedCompilers, compiler) then
|
||||
print("Invalid compiler: "..compiler)
|
||||
error(usage, 0)
|
||||
end
|
||||
|
||||
local compilerIsVS = compiler:startswith("vs")
|
||||
|
||||
--
|
||||
-- Make sure I've got what I've need to be happy.
|
||||
--
|
||||
|
||||
local required = { "git" }
|
||||
|
||||
if not compilerIsVS then
|
||||
table.insert(required, "make")
|
||||
table.insert(required, compiler)
|
||||
end
|
||||
|
||||
for _, value in ipairs(required) do
|
||||
local z = execQuiet("%s --version", value)
|
||||
if not z then
|
||||
error("required tool '" .. value .. "' not found", 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--
|
||||
-- Figure out what I'm making.
|
||||
--
|
||||
|
||||
os.chdir("..")
|
||||
local text = os.outputof(string.format('git show %s:src/host/premake.h', branch))
|
||||
local _, _, version = text:find('VERSION%s*"([%w%p]+)"')
|
||||
|
||||
local pkgName = "premake-" .. version
|
||||
local pkgExt = ".zip"
|
||||
if kind == "binary" then
|
||||
pkgName = pkgName .. "-" .. os.host()
|
||||
if not os.istarget("windows") then
|
||||
pkgExt = ".tar.gz"
|
||||
end
|
||||
else
|
||||
pkgName = pkgName .. "-src"
|
||||
end
|
||||
|
||||
|
||||
--
|
||||
-- Make sure I'm sure.
|
||||
--
|
||||
|
||||
printf("")
|
||||
printf("I am about to create a %s package", kind:upper())
|
||||
printf(" ...named release/%s%s", pkgName, pkgExt)
|
||||
printf(" ...from the %s branch", branch)
|
||||
printf("")
|
||||
printf("Does this look right to you? If so, press [Enter] to begin.")
|
||||
io.read()
|
||||
|
||||
|
||||
--
|
||||
-- Pull down the release branch.
|
||||
--
|
||||
|
||||
print("Preparing release folder")
|
||||
os.mkdir("release")
|
||||
os.chdir("release")
|
||||
os.rmdir(pkgName)
|
||||
|
||||
print("Cloning source code")
|
||||
local z = execQuiet("git clone .. %s -b %s --recurse-submodules --depth 1 --shallow-submodules", pkgName, branch)
|
||||
if not z then
|
||||
error("clone failed", 0)
|
||||
end
|
||||
|
||||
os.chdir(pkgName)
|
||||
|
||||
--
|
||||
-- Bootstrap Premake in the newly cloned repository
|
||||
--
|
||||
|
||||
print("Bootstrapping Premake...")
|
||||
if compilerIsVS then
|
||||
z = os.execute("Bootstrap.bat " .. compiler)
|
||||
else
|
||||
z = os.execute("make -j -f Bootstrap.mak " .. os.host())
|
||||
end
|
||||
if not z then
|
||||
error("Failed to Bootstrap Premake", 0)
|
||||
end
|
||||
local premakeBin = path.translate("./bin/release/premake5")
|
||||
|
||||
|
||||
--
|
||||
-- Make absolutely sure the embedded scripts have been updated
|
||||
--
|
||||
|
||||
print("Updating embedded scripts...")
|
||||
|
||||
local z = execQuiet("%s embed %s", premakeBin, iif(kind == "source", "", "--bytecode"))
|
||||
if not z then
|
||||
error("failed to update the embedded scripts", 0)
|
||||
end
|
||||
|
||||
|
||||
--
|
||||
-- Generate a source package.
|
||||
--
|
||||
|
||||
if kind == "source" then
|
||||
|
||||
local function genProjects(parameters)
|
||||
if not execQuiet("%s %s", premakeBin, parameters) then
|
||||
error("failed to generate project for "..parameters, 0)
|
||||
end
|
||||
end
|
||||
|
||||
os.rmdir("build")
|
||||
|
||||
print("Generating project files...")
|
||||
|
||||
local ignoreActions = {
|
||||
"clean",
|
||||
"embed",
|
||||
"package",
|
||||
"self-test",
|
||||
"test",
|
||||
"gmake", -- deprecated
|
||||
}
|
||||
|
||||
local perOSActions = {
|
||||
"gmake2",
|
||||
"codelite"
|
||||
}
|
||||
|
||||
for action in premake.action.each() do
|
||||
|
||||
if not table.contains(ignoreActions, action.trigger) then
|
||||
if table.contains(perOSActions, action.trigger) then
|
||||
|
||||
local osList = {
|
||||
{ "windows", },
|
||||
{ "unix", "linux" },
|
||||
{ "macosx", },
|
||||
{ "bsd", },
|
||||
}
|
||||
|
||||
for _, os in ipairs(osList) do
|
||||
local osTarget = os[2] or os[1]
|
||||
genProjects(string.format("--to=build/%s.%s --os=%s %s", action.trigger, os[1], osTarget, action.trigger))
|
||||
end
|
||||
else
|
||||
genProjects(string.format("--to=build/%s %s", action.trigger, action.trigger))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print("Creating source code package...")
|
||||
|
||||
local excludeList = {
|
||||
".gitignore",
|
||||
".gitattributes",
|
||||
".gitmodules",
|
||||
".travis.yml",
|
||||
".editorconfig",
|
||||
"appveyor.yml",
|
||||
"Bootstrap.*",
|
||||
"packages/*",
|
||||
}
|
||||
local includeList = {
|
||||
"build",
|
||||
"src/scripts.c",
|
||||
}
|
||||
|
||||
if not execQuiet("git rm --cached -r -f --ignore-unmatch "..table.concat(excludeList, ' ')) or
|
||||
not execQuiet("git add -f "..table.concat(includeList, ' ')) or
|
||||
not execQuiet("git stash") or
|
||||
not execQuiet("git archive --format=zip -9 -o ../%s.zip --prefix=%s/ stash@{0}", pkgName, pkgName) or
|
||||
not execQuiet("git stash drop stash@{0}")
|
||||
then
|
||||
error("failed to archive release", 0)
|
||||
end
|
||||
|
||||
os.chdir("..")
|
||||
end
|
||||
|
||||
|
||||
--
|
||||
-- Create a binary package for this platform. This step requires a working
|
||||
-- GNU/Make/GCC environment.
|
||||
--
|
||||
|
||||
if kind == "binary" then
|
||||
|
||||
print("Building binary...")
|
||||
|
||||
os.chdir("bin/release")
|
||||
|
||||
local addCommand = "git add -f premake5%s"
|
||||
local archiveCommand = "git archive --format=%s -o ../../../%s%s stash@{0} -- ./premake5%s"
|
||||
|
||||
if os.ishost("windows") then
|
||||
addCommand = string.format(addCommand, ".exe")
|
||||
archiveCommand = string.format(archiveCommand, "zip -9", pkgName, pkgExt, ".exe")
|
||||
else
|
||||
addCommand = string.format(addCommand, "")
|
||||
archiveCommand = string.format(archiveCommand, "tar.gz", pkgName, pkgExt, "")
|
||||
end
|
||||
|
||||
if not execQuiet(addCommand) or
|
||||
not execQuiet("git stash") or
|
||||
not execQuiet(archiveCommand) or
|
||||
not execQuiet("git stash drop stash@{0}")
|
||||
then
|
||||
error("failed to archive release", 0)
|
||||
end
|
||||
|
||||
os.chdir("../../..")
|
||||
|
||||
end
|
||||
|
||||
|
||||
--
|
||||
-- Clean up
|
||||
--
|
||||
|
||||
-- Use RMDIR token instead of os.rmdir to force remove .git dir which has read only files
|
||||
execQuiet(os.translateCommands("{RMDIR} "..pkgName))
|
Loading…
Add table
Add a link
Reference in a new issue