summaryrefslogtreecommitdiff
path: root/package.py
diff options
context:
space:
mode:
Diffstat (limited to 'package.py')
-rwxr-xr-xpackage.py219
1 files changed, 219 insertions, 0 deletions
diff --git a/package.py b/package.py
new file mode 100755
index 00000000..721bb0eb
--- /dev/null
+++ b/package.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python3
+
+from fnmatch import fnmatch
+from os import path
+import argparse
+import json
+import os
+import re
+import subprocess
+import sys
+import time
+import urllib.request
+import zipfile
+
+# Configuration
+
+## Where do we fetch the list of releases from?
+RELEASES_URL = "https://api.github.com/repos/Z3Prover/z3/releases/latest"
+## How do we extract info from the name of a release file?
+RELEASE_REGEXP = re.compile(r"^(?P<directory>z3-[0-9\.]+-(?P<platform>x86|x64)-(?P<os>[a-z0-9\.\-]+)).zip$", re.IGNORECASE)
+
+## Where are the sources?
+SOURCE_DIRECTORY = "Source"
+## Where do the binaries get put?
+BINARIES_DIRECTORY = "Binaries"
+## Where do we store the built packages and cache files?
+DESTINATION_DIRECTORY = "Package"
+
+## What's the root folder of the archive?
+DAFNY_PACKAGE_PREFIX = path.join("dafny")
+## What sub-folder of the packages does z3 go into?
+Z3_PACKAGE_PREFIX = path.join("z3")
+
+## What do we take from the z3 archive? (Glob syntax)
+Z3_INTERESTING_FILES = ["LICENSE.txt", "bin/*"]
+
+## On unix systems, which Dafny files should be marked as executable? (Glob syntax; Z3's permissions are preserved)
+UNIX_EXECUTABLES = ["dafny", "dafny-server"]
+
+## What do we take from Dafny's Binaries folder?
+DLLs = ["AbsInt",
+ "Basetypes",
+ "CodeContractsExtender",
+ "Concurrency",
+ "Core",
+ "DafnyPipeline",
+ "Doomed",
+ "ExecutionEngine",
+ "Graph",
+ "Houdini",
+ "Model",
+ "ModelViewer",
+ "ParserHelper",
+ "Provers.SMTLib",
+ "VCExpr",
+ "VCGeneration"]
+EXEs = ["Dafny", "DafnyServer"]
+ETCs = UNIX_EXECUTABLES + ["DafnyPrelude.bpl", "DafnyRuntime.cs", "DafnyLanguageService.vsix"]
+
+# Constants
+
+THIS_FILE = path.realpath(__file__)
+ROOT_DIRECTORY = path.dirname(THIS_FILE)
+SOURCE_DIRECTORY = path.join(ROOT_DIRECTORY, SOURCE_DIRECTORY)
+BINARIES_DIRECTORY = path.join(ROOT_DIRECTORY, BINARIES_DIRECTORY)
+DESTINATION_DIRECTORY = path.join(ROOT_DIRECTORY, DESTINATION_DIRECTORY)
+CACHE_DIRECTORY = path.join(DESTINATION_DIRECTORY, "cache")
+
+MONO = sys.platform not in ("win32", "cygwin")
+DLL_PDB_EXT = ".dll.mdb" if MONO else ".pdb"
+EXE_PDB_EXT = ".exe.mdb" if MONO else ".pdb"
+ARCHIVE_FNAMES = ([dll + ".dll" for dll in DLLs] + [dll + DLL_PDB_EXT for dll in DLLs] +
+ [exe + ".exe" for exe in EXEs] + [exe + EXE_PDB_EXT for exe in EXEs] +
+ ETCs)
+
+# Code
+
+def flush(*args, **kwargs):
+ print(*args, **kwargs)
+ sys.stdout.flush()
+
+class Release:
+ @staticmethod
+ def parse_zip_name(name):
+ m = RELEASE_REGEXP.match(name)
+ if not m:
+ raise Exception("{} does not match RELEASE_REGEXP".format(name))
+ return m.group('platform'), m.group('os'), m.group("directory")
+
+ def __init__(self, js, version):
+ self.z3_name = js["name"]
+ self.size = js["size"]
+ self.url = js["browser_download_url"]
+ self.platform, self.os, self.directory = Release.parse_zip_name(js["name"])
+ self.z3_zip = path.join(CACHE_DIRECTORY, self.z3_name)
+ self.dafny_name = "dafny-{}-{}-{}.zip".format(version, self.platform, self.os)
+ self.dafny_zip = path.join(DESTINATION_DIRECTORY, self.dafny_name)
+
+ @property
+ def cached(self):
+ return path.exists(self.z3_zip) and path.getsize(self.z3_zip) == self.size
+
+ @property
+ def MB(self):
+ return self.size / 1e6
+
+ def download(self):
+ if self.cached:
+ print("cached!")
+ else:
+ flush("downloading {:.2f}MB...".format(self.MB), end=' ')
+ with urllib.request.urlopen(self.url) as reader:
+ with open(self.z3_zip, mode="wb") as writer:
+ writer.write(reader.read())
+ flush("done!")
+
+ @staticmethod
+ def zipify_path(fpath):
+ """Zip entries always use '/' as the path separator."""
+ return fpath.replace(os.path.sep, '/')
+
+ def pack(self):
+ try:
+ os.remove(self.dafny_zip)
+ except FileNotFoundError:
+ pass
+ missing = []
+ with zipfile.ZipFile(self.dafny_zip, 'w', zipfile.ZIP_DEFLATED) as archive:
+ with zipfile.ZipFile(self.z3_zip) as Z3_archive:
+ z3_files_count = 0
+ for fileinfo in Z3_archive.infolist():
+ fname = path.relpath(fileinfo.filename, self.directory)
+ if any(fnmatch(fname, pattern) for pattern in Z3_INTERESTING_FILES):
+ z3_files_count += 1
+ contents = Z3_archive.read(fileinfo)
+ fileinfo.filename = Release.zipify_path(path.join(DAFNY_PACKAGE_PREFIX, Z3_PACKAGE_PREFIX, fname))
+ archive.writestr(fileinfo, contents)
+ for fname in ARCHIVE_FNAMES:
+ fpath = path.join(BINARIES_DIRECTORY, fname)
+ if path.exists(fpath):
+ fileinfo = zipfile.ZipInfo(fname, time.localtime(os.stat(fpath).st_mtime)[:6])
+ if any(fnmatch(fname, pattern) for pattern in UNIX_EXECUTABLES):
+ # http://stackoverflow.com/questions/434641/
+ fileinfo.external_attr = 0o777 << 16
+ contents = open(fpath, mode='rb').read()
+ fileinfo.compress_type = zipfile.ZIP_DEFLATED
+ fileinfo.filename = Release.zipify_path(path.join(DAFNY_PACKAGE_PREFIX, fname))
+ archive.writestr(fileinfo, contents)
+ else:
+ missing.append(fname)
+ flush("done! (imported {} files from z3's sources)".format(z3_files_count))
+ if missing:
+ flush(" WARNING: Not all files were found: {} were missing".format(", ".join(missing)))
+
+def discover(version):
+ flush(" - Getting information about latest release")
+ with urllib.request.urlopen(RELEASES_URL) as reader:
+ js = json.loads(reader.read().decode("utf-8"))
+
+ for release_js in js["assets"]:
+ release = Release(release_js, version)
+ if release.platform == "x64":
+ flush(" + Selecting {} ({:.2f}MB, {})".format(release.z3_name, release.MB, release.size))
+ yield release
+ else:
+ flush(" + Rejecting {}".format(release.z3_name))
+
+def download(releases):
+ flush(" - Downloading {} z3 archives".format(len(releases)))
+ for release in releases:
+ flush(" + {}:".format(release.z3_name), end=' ')
+ release.download()
+
+def run(cmd):
+ flush(" + {}...".format(" ".join(cmd)), end=' ')
+ retv = subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ if retv != 0:
+ flush("failed! (Is Dafny or the Dafny server running?)")
+ sys.exit(1)
+ else:
+ flush("done!")
+
+def build():
+ os.chdir(ROOT_DIRECTORY)
+ flush(" - Building")
+ builder = "xbuild" if MONO else "msbuild"
+ try:
+ run([builder, "Source/Dafny.sln", "/p:Configuration=Checked", "/p:Platform=Any CPU", "/t:Clean"])
+ run([builder, "Source/Dafny.sln", "/p:Configuration=Checked", "/p:Platform=Any CPU", "/t:Rebuild"])
+ except FileNotFoundError:
+ flush("Could not find '{}'! On Windows, you need to run this from the VS native tools command prompt.".format(builder))
+ sys.exit(1)
+
+def pack(releases):
+ flush(" - Packaging {} Dafny archives".format(len(releases)))
+ for release in releases:
+ flush(" + {}:".format(release.dafny_name), end=' ')
+ release.pack()
+
+def parse_arguments():
+ parser = argparse.ArgumentParser(description="Prepare a Dafny release. Configuration is hardcoded; edit the `# Configuration' section of this script to change it.")
+ parser.add_argument("version", help="Version number for this release")
+ return parser.parse_args()
+
+def main():
+ args = parse_arguments()
+ os.makedirs(CACHE_DIRECTORY, exist_ok=True)
+
+ # Z3
+ flush("* Finding and downloading Z3 releases")
+ releases = list(discover(args.version))
+ download(releases)
+
+ flush("* Building and packaging Dafny")
+ build()
+ pack(releases)
+
+if __name__ == '__main__':
+ main()