From c0a38078166603d8bfc7230ef6e58a75bd6ca9ca Mon Sep 17 00:00:00 2001 From: Doekin <105162544+Doekin@users.noreply.github.com> Date: Tue, 31 Dec 2024 23:00:54 +0800 Subject: [PATCH] OpenSSL: improve MinGW support on Windows (#6079) * openssl: enhance test * openssl: improve cross-build support on Windows * openssl: Remove always-true `if` condition * openssl: handle mismatched Perl * openssl: fix Lua script * openssl: fix removing DLL import libraries unexpectedly * openssl: fix BSD platform * openssl: fix wrong config * openssl: skip building tests * openssl: improve Perl dependency handling for Windows * openssl: fix overlong make recipe * openssl: trim trailing whitespace * openssl: improve Perl platform compatibility warning message * openssl: ensure ARFLAGS is not overridden with an empty string * openssl: restore the check for precompiled packages * openssl: add LLVM resource compiler support for MinGW * openssl: remove temporary file after use * openssl: add try-catch to configuration and Makefile patch functions * openssl: improve compatibility with older versions * openssl: add warning for building versions earlier than 1.1.1 * openssl: improve version checks --- packages/o/openssl/configure/patch.lua | 170 +++++++++++++++++++++++ packages/o/openssl/makefile/patch.lua | 36 +++++ packages/o/openssl/xmake.lua | 185 +++++++++++++++++-------- 3 files changed, 331 insertions(+), 60 deletions(-) create mode 100644 packages/o/openssl/configure/patch.lua create mode 100644 packages/o/openssl/makefile/patch.lua diff --git a/packages/o/openssl/configure/patch.lua b/packages/o/openssl/configure/patch.lua new file mode 100644 index 00000000000..5070202c843 --- /dev/null +++ b/packages/o/openssl/configure/patch.lua @@ -0,0 +1,170 @@ +function _fix_overlong_make_recipe(package) + -- In the MSYS environment, the make recipe can be too long to execute. + -- This patch is adapted from OpenSSL 3. + -- For more details, see: https://github.com/openssl/openssl/issues/12116 + io.gsub("Configurations/00-base-templates.conf", -- replace default AR + + "DEFAULTS%s-=>%s-{" .. + "(.-)".. + [[AR%s-=>%s-"%S-"%s-,]].. -- AR => "ar", + "(.-)}", + + "DEFAULTS => {".. + "%1".. + [[AR => "(unused)",]] .. + "%2}") + io.gsub("Configurations/00-base-templates.conf", -- replace default ARFLAGS + + "DEFAULTS%s-=>%s-{" .. + "(.-)".. + [[ARFLAGS%s-=>%s-"%S-"%s-,]].. -- ARFLAGS => "r", + "(.-)}", + + "DEFAULTS => {".. + "%1".. + [[ARFLAGS => "(unused)",]] .. + "%2}") + io.gsub("Configurations/00-base-templates.conf", -- replace BASE_unix ARFLAGS + + "BASE_unix%s-=>%s-{" .. + "(.-)".. + [[ARFLAGS%s-=>%s-"%S-"%s-,]].. -- ARFLAGS => "r", + "(.-)}", + + "BASE_unix => {".. + "%1".. + [[ARFLAGS => "qc",]] .. + "%2}") + io.gsub("Configurations/unix-Makefile.tmpl", -- insert fill_lines function + + "(sub%s-dependmagic%s-{)" .. + "(.-)".. + "}%s-'';", + + "%1".. + "%2".. + "}\n".. + [[ + sub fill_lines { + my $item_sep = shift; # string + my $line_length = shift; # number of chars + + my @result = (); + my $resultpos = 0; + + foreach (@_) { + my $fill_line = $result[$resultpos] // ''; + my $newline = + ($fill_line eq '' ? '' : $fill_line . $item_sep) . $_; + + if (length($newline) > $line_length) { + # If this is a single item and the intended result line + # is empty, we put it there anyway + if ($fill_line eq '') { + $result[$resultpos++] = $newline; + } else { + $result[++$resultpos] = $_; + } + } else { + $result[$resultpos] = $newline; + } + } + return @result; + } + ]].. + [['';]]) + + io.gsub("Configurations/unix-Makefile.tmpl", -- change the way we handle dependencies + + "sub%s-libobj2shlib%s-{" .. + "(.-)".. + [[my%s-%$objs.-;]].. -- my $objs = join(" ", @objs); + "(.-)}", + + "sub libobj2shlib {".. + "%1".. + [[my $objs = + join(" \\\n\t\t", fill_lines(' ', $COLUMNS - 16, @objs));]] .. + "%2}") + io.gsub("Configurations/unix-Makefile.tmpl", -- change the way we handle dependencies + + "sub%s-libobj2shlib%s-{" .. + "(.-)".. + [[my%s-%$deps.-;]].. -- my $deps = join(" ", @objs, @defs, @deps); + "(.-)}", + + "sub libobj2shlib {".. + "%1".. + [[my @fulldeps = (@objs, @defs, @deps); + my $fulldeps = + join(" \\\n" . ' ' x (length($full) + 2), + fill_lines(' ', $COLUMNS - length($full) - 2, @fulldeps));]] .. + "%2}") + io.gsub("Configurations/unix-Makefile.tmpl", + + "sub%s-libobj2shlib%s-{" .. + "(.-)".. + [[%$target:%s-%$deps]].. -- $target: $deps + "(.-)}", + + "sub libobj2shlib {".. + "%1".. + [[$target: $fulldeps]] .. + "%2}") + io.gsub("Configurations/unix-Makefile.tmpl", + + "sub%s-obj2lib%s-{" .. + "(.-)".. + [[my%s-%$objs.-;]].. -- my $objs = join(" ", @objs); + "(.-)}", + + "sub obj2lib {".. + "%1".. + [[my $deps = join(" \\\n" . ' ' x (length($lib) + 2), + fill_lines(' ', $COLUMNS - length($lib) - 2, @objs)); + my $max_per_call = 250; + my @objs_grouped; + push @objs_grouped, join(" ", splice @objs, 0, $max_per_call) while @objs; + my $fill_lib = + join("\n\t", (map { "\$(AR) \$(ARFLAGS) $lib$libext $_" } @objs_grouped));]] .. + "%2}") + io.gsub("Configurations/unix-Makefile.tmpl", + + "sub%s-obj2lib%s-{" .. + "(.-)".. + [[%$lib%$libext:.-]].. -- $lib$libext: $objs + "EOF", + + "sub obj2lib {".. + "%1".. + "$lib$libext: $deps\n" .. + '\t' .. [[\$(RM) $lib$libext]] ..'\n' .. + '\t' .. [[$fill_lib]] ..'\n' .. + '\t' .. [[\$(RANLIB) \$\@ || echo Never mind.]] .. '\n' .. + "EOF") +end + +function _remove_unused_pod_usage(package) + -- Perl in "Git for Windows" lacks Pod::Usage, which is only used for help messages in the Configure script. + -- It is not needed for the build and can be safely removed to avoid errors from the missing module. + if package:version():le("1.1.0") then + return + end + io.replace("Configure", "use Pod::Usage;", "", {plain = true}) + io.replace("Configure", "pod2usage.-;", "") +end + +function _replace_NUL_with_null(package) + -- The Configure script uses "NUL" to redirect output on Windows when checking NASM. + -- Creating a file named "NUL" can cause issues because "NUL" is a reserved name in Windows. + if package:version():le("1.1.0") then + return + end + io.replace("Configurations/10-main.conf", "NUL", "null", {plain = true}) +end + +function main(package) + _remove_unused_pod_usage(package) + _replace_NUL_with_null(package) + _fix_overlong_make_recipe(package) +end diff --git a/packages/o/openssl/makefile/patch.lua b/packages/o/openssl/makefile/patch.lua new file mode 100644 index 00000000000..f94b81425c1 --- /dev/null +++ b/packages/o/openssl/makefile/patch.lua @@ -0,0 +1,36 @@ +function _patch_for_llvm_rc(package, opt) + if package:is_plat("mingw") and package:has_tool("mrc", "llvm_rc", "llvm-rc", "rc") then + local cc = package:build_getenv("cc") + local cflags = opt.buildenvs.CFLAGS + local tmpfile = path.unix(os.tmpfile() .. ".c") + io.writefile(tmpfile, "int main(void) { return 0; }\n") + local compile_out, compile_err = try {function() return os.iorun(format("%s -v %s %s", cc, cflags, tmpfile)) end} + os.tryrm(tmpfile) + local include_dirs = {} + local in_include_section = false + for _, verbose_command in ipairs({compile_out, compile_err}) do + if verbose_command then + for line in verbose_command:gmatch("[^\r\n]+") do + if line:find("#include.*search") then + in_include_section = true + elseif line:find("End.*search") then + in_include_section = false + elseif in_include_section and not line:find("#include.*search") then + table.insert(include_dirs, line:match("^%s*(.-)%s*$")) + end + end + end + end + local include_directive = "" + for _, include_dir in ipairs(include_dirs) do + include_directive = include_directive .. format([[ -I "%s"]], include_dir) + end + -- $(RC) $(RCFLAGS) -o $@ libcrypto.rc => $(RC) -I inc_dir -I inc_dir -FO $@ libcrypto.rc + io.gsub("Makefile", [[%$%(RC%).-%$@%s+(%S+)]], format("$(RC) %s -FO $@ ", include_directive).."%1") + io.gsub("Makefile", "(%S+).res.o", "%1.res") + end +end + +function main(package, opt) + try {function() return _patch_for_llvm_rc(package, opt) end} +end diff --git a/packages/o/openssl/xmake.lua b/packages/o/openssl/xmake.lua index 64c06bc7662..34a29bf0e02 100644 --- a/packages/o/openssl/xmake.lua +++ b/packages/o/openssl/xmake.lua @@ -32,15 +32,18 @@ package("openssl") if package:is_plat("android") and is_subhost("windows") and os.arch() == "x64" then -- when building for android on windows, use msys2 perl instead of strawberry-perl to avoid configure issue package:add("deps", "msys2", {configs = {msystem = "MINGW64", base_devel = true}, private = true}) - elseif is_subhost("windows") and not package:is_precompiled() then - package:add("deps", "nasm", { private = true }) - -- the perl executable found in GitForWindows will fail to build OpenSSL - -- see https://github.com/openssl/openssl/blob/master/NOTES-PERL.md#perl-on-windows - package:add("deps", "strawberry-perl", { system = false, private = true }) - -- check xmake tool jom - import("package.tools.jom", {try = true}) - if jom then - package:add("deps", "jom", {private = true}) + elseif is_subhost("windows") then + package:add("deps", "strawberry-perl", { private = true }) + if package:is_plat("windows") then + package:add("deps", "nasm", { private = true }) + -- check xmake tool jom + import("package.tools.jom", {try = true}) + if jom then + package:add("deps", "jom", {private = true}) + end + else + wprint("package(openssl): OpenSSL should be built in a Unix-like shell (e.g., MSYS2, Git Bash) if not targeting MSVC. " .. + "It seems you are not in such an environment. If you encounter build errors, please consider trying again in a Unix-like shell.") end end end @@ -61,10 +64,31 @@ package("openssl") end end) + if on_check then + on_check(function (package) + local working_dir = try {function() return os.iorunv("perl", {"-MFile::Spec::Functions=rel2abs", "-e", "print rel2abs('.')"}) end} + assert(working_dir, "package(openssl): perl not found!") + -- Check if Perl is using Unix-style paths + local unix_perl = working_dir:find("/") == 1 + if (unix_perl and package:is_plat("windows")) or (not unix_perl and not package:is_plat("windows")) then + wprint("package(openssl): Detected Perl may not match your build platform. ".. + "If you encounter build issues, ensure you have the correct Perl installed ".. + "and it takes priority in your system.") + end + + if package:version():le("1.1.0") then + wprint("package(openssl): Building OpenSSL versions earlier than 1.1.1 may fail due to unresolved bugs. If you encounter build issues, please consider using a newer version.") + end + end) + end + on_install("windows", function (package) import("package.tools.jom", {try = true}) import("package.tools.nmake") - local configs = {"Configure", "no-tests"} + local configs = {"Configure"} + if not package:version():le("1.1.0") then + table.insert(configs, "no-tests") + end local target if package:is_arch("x86", "i386") then target = "VC-WIN32" @@ -79,13 +103,14 @@ package("openssl") table.insert(configs, package:config("shared") and "shared" or "no-shared") table.insert(configs, "--prefix=" .. package:installdir()) table.insert(configs, "--openssldir=" .. package:installdir()) - if jom then + if jom and not package:version():le("1.1.0") then table.insert(configs, "no-makedepend") table.insert(configs, "/FS") end + import("configure.patch")(package) os.vrunv("perl", configs) - if jom then + if jom and not package:version():le("1.1.0") then jom.build(package) jom.make(package, {"install_sw"}) else @@ -94,39 +119,10 @@ package("openssl") end end) - on_install("mingw", function (package) - local configs = {"Configure", "no-tests"} - table.insert(configs, package:is_arch("i386", "x86") and "mingw" or "mingw64") - table.insert(configs, package:config("shared") and "shared" or "no-shared") - local installdir = package:installdir() - -- Use MSYS2 paths instead of Windows paths - if is_subhost("msys") then - installdir = installdir:gsub("(%a):[/\\](.+)", "/%1/%2"):gsub("\\", "/") - end - table.insert(configs, "--prefix=" .. installdir) - table.insert(configs, "--openssldir=" .. installdir) - local buildenvs = import("package.tools.autoconf").buildenvs(package) - buildenvs.RC = package:build_getenv("mrc") - if is_subhost("msys") then - local rc = buildenvs.RC - if rc then - rc = rc:gsub("(%a):[/\\](.+)", "/%1/%2"):gsub("\\", "/") - buildenvs.RC = rc - end - end - -- fix 'cp: directory fuzz does not exist' - if package:config("shared") then - os.mkdir("fuzz") - end - os.vrunv("perl", configs, {envs = buildenvs}) - import("package.tools.make").build(package) - import("package.tools.make").make(package, {"install_sw"}) - end) - - on_install("linux", "macosx", "bsd", "cross", "android", "iphoneos", function (package) + on_install("linux", "macosx", "bsd", "cross", "android", "iphoneos", "mingw", function (package) -- https://wiki.openssl.org/index.php/Compilation_and_Installation#PREFIX_and_OPENSSLDIR - local configs = {} - if package:is_cross() then + local configs = (not package:version():le("1.1.0")) and {"no-tests"} or {} + if package:is_cross() or package:is_plat("mingw") then local target_plat, target_arch if package:is_plat("macosx") then target_plat = "darwin64" @@ -145,6 +141,21 @@ package("openssl") end target_arch = "cross" end + elseif package:is_plat("mingw") then + target_plat = package:is_arch("i386", "x86") and "mingw" or "mingw64" + elseif package:is_plat("bsd") then + target_plat = "BSD" + if package:is_arch("i386") then + target_arch = "x86" + elseif package:is_arch("x86_64") then + target_arch = "x86_64" + elseif package:is_arch("arm64") then + target_arch = "aarch64" + elseif package:is_arch("riscv64") then + target_arch = "riscv64" + elseif package:is_arch(".*64") then + target_arch = "generic64" + end else target_plat = "linux" if package:is_arch("x86_64") then @@ -161,36 +172,90 @@ package("openssl") target_arch = "generic32" end end - table.insert(configs, target_plat .. "-" .. target_arch) - if package:is_plat("cross", "android") then - table.insert(configs, "-DOPENSSL_NO_HEARTBEATS") - table.insert(configs, "no-threads") - end + table.insert(configs, target_arch and (target_plat .. "-" .. target_arch) or target_plat) + end + if package:is_plat("cross", "android") then + table.insert(configs, "-DOPENSSL_NO_HEARTBEATS") + table.insert(configs, "no-threads") end - table.insert(configs, "--openssldir=" .. package:installdir():gsub("\\", "/")) - table.insert(configs, "--prefix=" .. package:installdir():gsub("\\", "/")) + local installdir = package:installdir() + -- Use MSYS2 paths instead of Windows paths + if is_subhost("msys") then + installdir = installdir:gsub("(%a):[/\\](.+)", "/%1/%2") + end + installdir = installdir:gsub("\\", "/") + table.insert(configs, "--openssldir=" .. installdir) + table.insert(configs, "--prefix=" .. installdir) table.insert(configs, package:config("shared") and "shared" or "no-shared") if package:debug() then table.insert(configs, "--debug") end - local buildenvs = import("package.tools.autoconf").buildenvs(package) - if package:is_cross() then - if is_host("windows") and package:is_plat("android") then - buildenvs.CFLAGS = buildenvs.CFLAGS:gsub("\\", "/") - buildenvs.CXXFLAGS = buildenvs.CXXFLAGS:gsub("\\", "/") - buildenvs.CPPFLAGS = buildenvs.CPPFLAGS:gsub("\\", "/") - buildenvs.ASFLAGS = buildenvs.ASFLAGS:gsub("\\", "/") + + local ldflags = {} + if package:is_plat("mingw") and package:has_tool("mrc", "llvm_rc", "llvm-rc", "rc") then + -- llvm-rc should be used with lld linker + if package:has_tool("cc", "clang") then + table.insert(ldflags, "-fuse-ld=lld") end + end + local buildenvs = import("package.tools.autoconf").buildenvs(package, {ldflags = ldflags}) + if is_host("windows") and package:is_plat("android") then + buildenvs.CFLAGS = buildenvs.CFLAGS:gsub("\\", "/") + buildenvs.CXXFLAGS = buildenvs.CXXFLAGS:gsub("\\", "/") + buildenvs.CPPFLAGS = buildenvs.CPPFLAGS:gsub("\\", "/") + buildenvs.ASFLAGS = buildenvs.ASFLAGS:gsub("\\", "/") + end + -- Check if ARFLAGS exists and is empty or contains only whitespace + if buildenvs.ARFLAGS and buildenvs.ARFLAGS:match("^%s*$") then + buildenvs.ARFLAGS = nil + end + if package:is_plat("mingw") then + buildenvs.RC = package:build_getenv("mrc") + if is_subhost("msys") and buildenvs.RC then + buildenvs.RC = buildenvs.RC:gsub("(%a):[/\\](.+)", "/%1/%2"):gsub("\\", "/") + end + -- fix 'cp: directory fuzz does not exist' + if package:config("shared") then + os.mkdir("fuzz") + end + end + + import("configure.patch")(package) + if package:is_cross() or package:is_plat("mingw") then os.vrunv("perl", table.join("./Configure", configs), {envs = buildenvs}) else os.vrunv("./config", configs, {shell = true, envs = buildenvs}) end + + import("makefile.patch")(package, {buildenvs = buildenvs}) import("package.tools.make").build(package) import("package.tools.make").make(package, {"install_sw"}) if package:config("shared") then - os.tryrm(path.join(package:installdir("lib"), "*.a")) + os.tryrm(path.join(package:installdir("lib"), "*.a|*.dll.a")) end end) on_test(function (package) - assert(package:has_cfuncs("SSL_new", {includes = "openssl/ssl.h"})) + assert(package:check_csnippets({test = [[ + #include + + int test(){ + SSL_library_init(); + SSL_load_error_strings(); + SSL_CTX *ctx = SSL_CTX_new(SSLv23_client_method()); + if(ctx == NULL){ + return 1; + } + + SSL *ssl = SSL_new(ctx); + if(ssl == NULL){ + SSL_CTX_free(ctx); + return 1; + } + + SSL_free(ssl); + SSL_CTX_free(ctx); + + return 0; + } + ]]})) end)