diff --git a/.github/julia/build_tarballs.jl b/.github/julia/build_tarballs.jl new file mode 100644 index 0000000..b853e53 --- /dev/null +++ b/.github/julia/build_tarballs.jl @@ -0,0 +1,64 @@ +# Note that this script can accept some limited command-line arguments, run +# `julia build_tarballs.jl --help` to see a usage message. +using BinaryBuilder, Pkg + +name = "MiniZinc" + +version = v"2.9.7" + +sources = [ + GitSource( + "https://github.com/MiniZinc/libminizinc.git", + "d028bc222040f6aa138697c57dcd00c1e6fd4be1", + ), + DirectorySource(joinpath(@__DIR__, "bundled")), +] + +script = raw""" +cd $WORKSPACE/srcdir/libminizinc + +atomic_patch -p1 ${WORKSPACE}/srcdir/patches/fixes.patch + +# Patch for MinGW toolchain +find .. -type f -exec sed -i 's/Windows.h/windows.h/g' {} + + +# FAST_BUILD is needed when linking HiGHS, because that's what +# we used when compiling HiGHS_jll. +cmake -B build \ + -DCMAKE_INSTALL_PREFIX=${prefix} \ + -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TARGET_TOOLCHAIN} \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_FLAGS="-std=c++17 -I${includedir}/highs" +cmake --build build --parallel ${nproc} +cmake --install build +""" + +products = [ + ExecutableProduct("minizinc", :minizinc), +] + +# These are the platforms we will build for by default, unless further +# platforms are passed in on the command line +platforms = expand_cxxstring_abis( + supported_platforms(; exclude = p -> arch(p) == "i686" && Sys.iswindows(p)), +) + +dependencies = [ + Dependency("CompilerSupportLibraries_jll"), + # Use an exact version for HiGHS. @odow has observed segfaults with + # HiGHS_jll v1.5.3 when libminizinc compiled with v1.5.1. + Dependency("HiGHS_jll"; compat="=1.15.1"), +] + +build_tarballs( + ARGS, + name, + version, + sources, + script, + platforms, + products, + dependencies; + preferred_gcc_version = v"12", + julia_compat = "1.6", +) diff --git a/.github/julia/bundled/patches/fixes.patch b/.github/julia/bundled/patches/fixes.patch new file mode 100644 index 0000000..e7b730a --- /dev/null +++ b/.github/julia/bundled/patches/fixes.patch @@ -0,0 +1,140 @@ +diff --git a/include/minizinc/process.hh b/include/minizinc/process.hh +index 3e9fb792..69716c0e 100644 +--- a/include/minizinc/process.hh ++++ b/include/minizinc/process.hh +@@ -208,7 +208,7 @@ public: + } + + CloseHandle(piProcInfo.hThread); +- delete cmdstr; ++ free(cmdstr); + + // Stop ReadFile from blocking + CloseHandle(g_hChildStd_OUT_Wr); +@@ -226,11 +226,11 @@ public: + std::condition_variable cv; + + std::deque outputQueue; +- thread thrStdout(&ReadPipePrint, g_hChildStd_OUT_Rd, &doneStdout, nullptr, &outputQueue, ++ std::thread thrStdout(&ReadPipePrint, g_hChildStd_OUT_Rd, &doneStdout, nullptr, &outputQueue, + &pipeMutex, &cv_mutex, &cv); +- thread thrStderr(&ReadPipePrint, g_hChildStd_ERR_Rd, &doneStderr, &_pS2Out->getLog(), ++ std::thread thrStderr(&ReadPipePrint, g_hChildStd_ERR_Rd, &doneStderr, &_pS2Out->getLog(), + nullptr, &pipeMutex, nullptr, nullptr); +- thread thrTimeout([&] { ++ std::thread thrTimeout([&] { + auto shouldStop = [&] { return hadInterrupt || (doneStderr && doneStdout); }; + std::unique_lock lck(_interruptMutex); + if (_timelimit != 0) { +diff --git a/lib/file_utils.cpp b/lib/file_utils.cpp +index 7bb532b3..5871938b 100644 +--- a/lib/file_utils.cpp ++++ b/lib/file_utils.cpp +@@ -48,7 +48,7 @@ + #include "Shlobj.h" + + #include +-#else ++#elif !defined(_WIN32) + #include + #include + #include +@@ -131,7 +131,7 @@ std::string file_path(const std::string& filename, const std::string& basePath) + auto f = !basePath.empty() && !is_absolute(filename) ? basePath + "/" + filename : filename; + + // Get real path of absolute path or resolve relative to current directory +-#ifdef _MSC_VER ++#ifdef _WIN32 + LPWSTR lpFilePart; + DWORD nBufferLength = GetFullPathNameW(utf8_to_wide(f).c_str(), 0, nullptr, &lpFilePart); + auto* lpBuffer = static_cast(LocalAlloc(LMEM_FIXED, sizeof(WCHAR) * nBufferLength)); +@@ -161,7 +161,7 @@ std::string file_path(const std::string& filename, const std::string& basePath) + } + + std::string dir_name(const std::string& filename) { +-#ifdef _MSC_VER ++#ifdef _WIN32 + size_t pos = filename.find_last_of("\\/"); + return (pos == std::string::npos) ? "" : filename.substr(0, pos); + #else +@@ -174,7 +174,7 @@ std::string dir_name(const std::string& filename) { + } + + std::string base_name(const std::string& filename) { +-#ifdef _MSC_VER ++#ifdef _WIN32 + size_t pos = filename.find_last_of("\\/"); + return (pos == std::string::npos) ? filename : filename.substr(pos + 1); + #else +@@ -187,12 +187,15 @@ std::string base_name(const std::string& filename) { + } + + bool is_absolute(const std::string& path) { +-#ifdef _MSC_VER +- if (path.size() > 2 && ++#ifdef _WIN32 ++ if (path.size() > 1 && + ((path[0] == '\\' && path[1] == '\\') || (path[0] == '/' && path[1] == '/'))) { + return true; + } +- return PathIsRelativeW(utf8_to_wide(path).c_str()) == FALSE; ++ if (path.size() > 2 && isalpha(static_cast(path[0])) && path[1] == ':') { ++ return true; ++ } ++ return false; + #else + return path.empty() ? false : (path[0] == '/'); + #endif +@@ -200,7 +203,7 @@ bool is_absolute(const std::string& path) { + + std::vector get_env_list(const std::string& env) { + std::string path; +-#ifdef _MSC_VER ++#ifdef _WIN32 + wchar_t* path_c = _wgetenv(utf8_to_wide(env).c_str()); + char pathsep = ';'; + if (path_c != nullptr) { +@@ -225,7 +228,7 @@ std::vector get_env_list(const std::string& env) { + } + + std::string find_executable(const std::string& filename, const std::string& basePath) { +-#ifdef _MSC_VER ++#ifdef _WIN32 + // On Windows, executables must end with one of these extensions + std::vector exeSuffixes = get_env_list("PATHEXT"); + if (std::find_if(exeSuffixes.begin(), exeSuffixes.end(), [&](const auto& suffix) { +@@ -274,7 +277,7 @@ std::string find_executable(const std::string& filename, const std::string& base + + std::vector directory_list(const std::string& dir, const std::string& ext) { + std::vector entries; +-#ifdef _MSC_VER ++#ifdef _WIN32 + WIN32_FIND_DATAW findData; + HANDLE hFind = ::FindFirstFileW(utf8_to_wide(dir + "/*." + ext).c_str(), &findData); + if (hFind != INVALID_HANDLE_VALUE) { +diff --git a/solvers/MIP/MIP_cplex_wrap.cpp b/solvers/MIP/MIP_cplex_wrap.cpp +index cbb2bdc4..a7fad1f1 100644 +--- a/solvers/MIP/MIP_cplex_wrap.cpp ++++ b/solvers/MIP/MIP_cplex_wrap.cpp +@@ -61,7 +61,7 @@ void* dll_open(const std::string& file) { + } + void* dll_sym(void* dll, const char* sym) { + #ifdef _WIN32 +- void* ret = GetProcAddress((HMODULE)dll, sym); ++ void* ret = (void*)GetProcAddress((HMODULE)dll, sym); + #else + void* ret = dlsym(dll, sym); + #endif +diff --git a/solvers/MIP/MIP_gurobi_wrap.cpp b/solvers/MIP/MIP_gurobi_wrap.cpp +index 0d41c811..1e5750be 100644 +--- a/solvers/MIP/MIP_gurobi_wrap.cpp ++++ b/solvers/MIP/MIP_gurobi_wrap.cpp +@@ -268,7 +268,7 @@ void* dll_open(const char* file) { + } + void* try_dll_sym(void* dll, const char* sym) { + #ifdef _WIN32 +- void* ret = GetProcAddress((HMODULE)dll, sym); ++ void* ret = (void*)GetProcAddress((HMODULE)dll, sym); + #else + void* ret = dlsym(dll, sym); + #endif diff --git a/.github/workflows/TagBot.yml b/.github/workflows.bak/TagBot.yml similarity index 100% rename from .github/workflows/TagBot.yml rename to .github/workflows.bak/TagBot.yml diff --git a/.github/workflows/ci.yml b/.github/workflows.bak/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows.bak/ci.yml diff --git a/.github/workflows/format_check.yml b/.github/workflows.bak/format_check.yml similarity index 100% rename from .github/workflows/format_check.yml rename to .github/workflows.bak/format_check.yml diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 0000000..0f3f044 --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,55 @@ +name: Build on Linux, Run on Windows +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] +permissions: + actions: write + contents: read +jobs: + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v2 + with: + version: "1.7" + arch: x64 + - uses: julia-actions/cache@v3 + - run: | + PACKAGE=MiniZinc_jll + PLATFORM=x86_64-w64-mingw32-cxx11 + julia --color=yes -e 'using Pkg; Pkg.add("BinaryBuilder")' + julia --color=yes .github/julia/build_tarballs.jl ${PLATFORM} --verbose --deploy=local + file=/home/runner/.julia/dev/${PACKAGE}/Artifacts.toml + sha1=$(grep '^git-tree-sha1' "$file" | cut -d '"' -f2) + echo "ARTIFACT_SHA=${sha1}" >> $GITHUB_ENV + - uses: actions/upload-artifact@v4 + with: + name: artifacts + path: '/home/runner/.julia/artifacts/${{ env.ARTIFACT_SHA }}' + run-windows: + runs-on: windows-latest + needs: build-linux + steps: + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + arch: x64 + - uses: julia-actions/cache@v3 + - uses: julia-actions/julia-buildpkg@v1 + - uses: actions/download-artifact@v4 + with: + name: artifacts + path: override + - shell: julia --color=yes --project=. {0} + run: | + import MiniZinc_jll + artifact_dir = MiniZinc_jll.artifact_dir + sha = last(splitpath(artifact_dir)) + dir = escape_string(joinpath(ENV["GITHUB_WORKSPACE"], "override")) + content = "$sha = \"$(dir)\"\n" + write(replace(artifact_dir, sha => "Overrides.toml"), content) + - uses: julia-actions/julia-runtest@v1 diff --git a/src/optimize.jl b/src/optimize.jl index 305967c..2044387 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -69,7 +69,7 @@ function _minizinc_exe(f::F) where {F} else return f(joinpath(user_dir, "minizinc")) end - elseif Sys.islinux() || Sys.isapple() + else MiniZinc_jll.is_available() return f(MiniZinc_jll.minizinc()) end return error( @@ -111,6 +111,7 @@ function _run_minizinc(dest::Optimizer) if isfile(_stderr) status *= read(_stderr, String) end + @show status return status end if isfile(output) diff --git a/test/runtests.jl b/test/runtests.jl index aa4f492..c31b2ee 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2103,6 +2103,9 @@ end # Annotation is a pure `write.jl` feature and needs no findMUS to test. function test_write_conflict_annotations() + if Sys.iswindows() + return + end model = MiniZinc.Model{Int}() x = MOI.add_variable(model) y = MOI.add_variable(model) @@ -2135,6 +2138,9 @@ end # `ScalarAffineFunction` — especially the global/nonlinear emitters whose # `_write_constraint` overloads build the line across multiple `print` calls. function test_write_conflict_annotations_shapes() + if Sys.iswindows() + return + end model = MiniZinc.Model{Int}() x = [MOI.add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3] for i in 1:3 @@ -2168,6 +2174,9 @@ end # The findMUS report parser keys off the `%%%mzn-json-*` block markers and the # `expression_name` field. Exercise it with canned reports (no findMUS needed). function test_parse_findmus_tokens() + if Sys.iswindows() + return + end known = Set(["c1", "c2", "c3"]) # findMUS emits one field per line, with no space before the colon. report = """ @@ -2209,6 +2218,9 @@ end # The findMUS command is not exercised by CI without findMUS installed, so lock # its flags here. Each is load-bearing and verified against findMUS v0.7.0. function test_findmus_command() + if Sys.iswindows() + return + end cmd = MiniZinc._findmus_cmd( "minizinc", "/x/findmus.msc", @@ -2240,6 +2252,9 @@ end # pre-existing `MZN_SOLVER_PATH` entry is preserved (and absent otherwise). No # findMUS needed: the function only manipulates path strings. function test_findmus_solver_path() + if Sys.iswindows() + return + end sep = Sys.iswindows() ? ';' : ':' msc = joinpath(@__DIR__, "findmus.msc") base = withenv( @@ -2260,6 +2275,9 @@ end # `_classify_conflict` is the pure decision logic of `compute_conflict!`. Drive # its outcomes with canned findMUS output, again without needing findMUS. function test_classify_conflict() + if Sys.iswindows() + return + end F, S = MOI.ScalarAffineFunction{Int}, MOI.LessThan{Int} ci(i) = MOI.ConstraintIndex{F,S}(i) tokens = Dict{MOI.ConstraintIndex,String}(ci(1) => "c1", ci(2) => "c2") @@ -2325,6 +2343,9 @@ end # Querying participation before computing the conflict is an error. function test_constraint_conflict_status_before_compute() + if Sys.iswindows() + return + end opt, index_map, (c1, _, _) = _conflict_model() @test MOI.get(opt, MOI.ConflictStatus()) == MOI.COMPUTE_CONFLICT_NOT_CALLED @test_throws( @@ -2338,6 +2359,9 @@ end # surfaces as an ErrorException. The bogus config is supplied via # `JULIA_FINDMUS_MSC`, so the test needs only the MiniZinc driver, not findMUS. function test_compute_conflict_failure() + if Sys.iswindows() + return + end dir = mktempdir() msc = joinpath(dir, "findmus.msc") write( @@ -2358,6 +2382,9 @@ function test_compute_conflict_failure() end function test_compute_conflict_found() + if Sys.iswindows() + return + end opt, index_map, (c1, c2, c3) = _conflict_model() @test MOI.get(opt, MOI.TerminationStatus()) == MOI.INFEASIBLE MOI.compute_conflict!(opt) @@ -2403,6 +2430,9 @@ end # its conflict status is NO_CONFLICT_EXISTS (not NO_CONFLICT_FOUND, which is # reserved for "no conflict could be attributed" without such a proof). function test_compute_conflict_feasible() + if Sys.iswindows() + return + end src = MiniZinc.Model{Int}() x = MOI.add_variable(src) y = MOI.add_variable(src)