diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6e546346 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/octoprint/_version.py export-subst diff --git a/MANIFEST.in b/MANIFEST.in index d47da275..8373cd4d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ recursive-include src/octoprint/static * recursive-include src/octoprint/templates * +include versioneer.py +include src/octoprint/_version.py diff --git a/setup.py b/setup.py index 5c407bc0..1dc91f38 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,13 @@ # coding=utf-8 #!/usr/bin/env python +import versioneer +versioneer.VCS = 'git' +versioneer.versionfile_source = 'src/octoprint/_version.py' +versioneer.versionfile_build = 'octoprint/_version.py' +versioneer.tag_prefix = '' +versioneer.parentdir_prefix = 'octoprint-' + from setuptools import setup, find_packages VERSION = open("VERSION").read().strip() @@ -51,6 +58,9 @@ def params(): # "scripts/octoprint.init": "/etc/init.d/octoprint" #} + version = versioneer.get_version() + cmdclass = versioneer.get_cmdclass() + return locals() setup(**params()) diff --git a/src/octoprint/__init__.py b/src/octoprint/__init__.py index 6d5dad46..67baa4f7 100644 --- a/src/octoprint/__init__.py +++ b/src/octoprint/__init__.py @@ -3,6 +3,13 @@ import sys from octoprint.daemon import Daemon from octoprint.server import Server +#~~ version + +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions + +#~~ main class class Main(Daemon): def __init__(self, pidfile, configfile, basedir, host, port, debug, allowRoot, logConf): @@ -25,6 +32,9 @@ def main(): parser = argparse.ArgumentParser(prog="run") + parser.add_argument("-v", "--version", action="store_true", dest="version", + help="Output OctoPrint's version and exit") + parser.add_argument("-d", "--debug", action="store_true", dest="debug", help="Enable debug mode") @@ -50,6 +60,10 @@ def main(): args = parser.parse_args() + if args.version: + print "OctoPrint version %s" % __version__ + sys.exit(0) + if args.daemon: if sys.platform == "darwin" or sys.platform == "win32": print >> sys.stderr, "Sorry, daemon mode is only supported under Linux right now" @@ -68,4 +82,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/src/octoprint/_version.py b/src/octoprint/_version.py new file mode 100644 index 00000000..f9a41364 --- /dev/null +++ b/src/octoprint/_version.py @@ -0,0 +1,188 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.10 (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "$Format:%d$" +git_full = "$Format:%H$" + + +import subprocess +import sys +import errno + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + assert isinstance(commands, list) + p = None + for c in commands: + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_abs): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + f = open(versionfile_abs,"r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs-tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, root, verbose=False): + # this runs 'git' from the root of the source tree. This only gets called + # if the git-archive 'subst' variables were *not* expanded, and + # _version.py hasn't already been rewritten with a short version string, + # meaning we're inside a checked out source tree. + + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, root, verbose=False): + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "" +parentdir_prefix = "octoprint-" +versionfile_source = "src/octoprint/_version.py" + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded variables. + + variables = { "refnames": git_refnames, "full": git_full } + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if ver: + return ver + + try: + root = os.path.abspath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + except NameError: + return default + + return (versions_from_vcs(tag_prefix, root, verbose) + or versions_from_parentdir(parentdir_prefix, root, verbose) + or default) + diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 9561f89a..18f2fa48 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -42,20 +42,15 @@ import octoprint.util as util import octoprint.users as users import octoprint.events as events import octoprint.timelapse +import octoprint._version UI_API_KEY = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes) +VERSION = octoprint._version.get_versions()['version'] @app.route("/") def index(): - branch = None - commit = None - try: - branch, commit = util.getGitInfo() - except: - pass - return render_template( "index.jinja2", webcamStream=settings().get(["webcam", "stream"]), @@ -67,8 +62,7 @@ def index(): enableSdSupport=settings().get(["feature", "sdSupport"]), firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()), debug=debug, - gitBranch=branch, - gitCommit=commit, + version=VERSION, stylesheet=settings().get(["devel", "stylesheet"]), gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]), gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]), @@ -139,6 +133,8 @@ class Server(): self._initLogging(self._debug, self._logConf) logger = logging.getLogger(__name__) + logger.info("Starting OctoPrint (%s)" % VERSION) + eventManager = events.eventManager() gcodeManager = gcodefiles.GcodeManager() printer = Printer(gcodeManager) diff --git a/src/octoprint/server/util.py b/src/octoprint/server/util.py index eff51a62..434a8d3b 100644 --- a/src/octoprint/server/util.py +++ b/src/octoprint/server/util.py @@ -155,7 +155,7 @@ class PrinterStateConnection(SockJSConnection): self._logger.info("New connection from client: %s" % remoteAddress) # connected => update the API key, might be necessary if the client was left open while the server restarted - self._emit("connected", {"apikey": octoprint.server.UI_API_KEY}) + self._emit("connected", {"apikey": octoprint.server.UI_API_KEY, "version": octoprint.server.VERSION}) self._printer.registerCallback(self) self._gcodeManager.registerCallback(self) diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index 577665e8..499a0ef2 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -77,6 +77,9 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM headers: {"X-Api-Key": UI_API_KEY} }); + VERSION = data["version"]; + $("span.version").text(VERSION); + if ($("#offline_overlay").is(":visible")) { $("#offline_overlay").hide(); self.logViewModel.requestData(); diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index 397a9049..505655f2 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -41,6 +41,7 @@ var SOCKJS_DEBUG = {% if debug -%} true; {% else %} false; {%- endif %} var UI_API_KEY = "{{ uiApiKey }}"; + var VERSION = "{{ version }}"; @@ -587,11 +588,9 @@