view add_make_release.patch @ 443:cf7f56964196

wip, reworked the client-side release commands
author Louis Opter <kalessin@kalessin.fr>
date Thu, 28 Apr 2016 23:50:40 -0700
parents 6615c130dab6
children 918645c7e15e
line wrap: on
line source

# HG changeset patch
# Parent  d16f6ed548379278b946b5ed7caf291ba0ac4e6e
Add a release command that can be used from buildbot

diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3,10 +3,10 @@
 
 PROJECT(LIGHTSD C)
 
-SET(CPACK_PACKAGE_VERSION_MAJOR "1")
-SET(CPACK_PACKAGE_VERSION_MINOR "2")
-SET(CPACK_PACKAGE_VERSION_PATCH "0")
-SET(LIGHTSD_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
+SET(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${LIGHTSD_SOURCE_DIR}/CMakeScripts")
+
+STRING(TOUPPER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE)
+INCLUDE(LightsdVersion)
 
 MESSAGE(STATUS "lightsd version: ${LIGHTSD_VERSION}")
 MESSAGE(STATUS "CMake version: ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}.${CMAKE_PATCH_VERSION}")
@@ -15,8 +15,6 @@
 MESSAGE(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}")
 MESSAGE(STATUS "Source directory: ${LIGHTSD_SOURCE_DIR}")
 
-SET(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${LIGHTSD_SOURCE_DIR}/CMakeScripts")
-
 ENABLE_TESTING()
 
 ### Platform checks ############################################################
@@ -26,6 +24,16 @@
 FIND_PACKAGE(Endian REQUIRED)
 FIND_PACKAGE(Sphinx)
 
+IF (CMAKE_BUILD_TYPE MATCHES RELEASE)
+  FIND_PACKAGE(Gzip)
+  FIND_PACKAGE(Hg)
+  SET(Python_ADDITIONAL_VERSIONS 3) # actually >= 3.3 (os.replace)
+  FIND_PACKAGE(PythonInterp)
+  FIND_PACKAGE(BsdTar)
+  FIND_PACKAGE(Virtualenv)
+  FIND_PACKAGE(Xz)
+ENDIF ()
+
 INCLUDE(CheckFunctionExists)
 INCLUDE(CheckVariableExists)
 INCLUDE(TestBigEndian)
@@ -92,9 +100,7 @@
 
 # 2.8.11 is the first version with TARGET_INCLUDE_DIRECTORIES:
 IF (CMAKE_VERSION VERSION_GREATER 2.8.10)
-    CONFIGURE_FILE(
-        CTestCustom.cmake.in "${LIGHTSD_BINARY_DIR}/CTestCustom.cmake" @ONLY
-    )
+    CONFIGURE_FILE(CTestCustom.cmake.in "${LIGHTSD_BINARY_DIR}/CTestCustom.cmake" @ONLY)
     ADD_SUBDIRECTORY(tests)
 ELSE ()
     MESSAGE(
@@ -105,7 +111,14 @@
 ENDIF ()
 
 IF (SPHINX_FOUND)
+    MESSAGE(STATUS "Sphinx found, docs generation enabled")
     ADD_SUBDIRECTORY(docs)
+ELSE ()
+    MESSAGE(STATUS "Shpinx wasn't found, docs generation disabled")
+ENDIF ()
+
+IF (CMAKE_BUILD_TYPE MATCHES RELEASE)
+  ADD_SUBDIRECTORY(dist)
 ENDIF ()
 
 INSTALL(
diff --git a/CMakeScripts/FindBsdTar.cmake b/CMakeScripts/FindBsdTar.cmake
new file mode 100644
--- /dev/null
+++ b/CMakeScripts/FindBsdTar.cmake
@@ -0,0 +1,6 @@
+FIND_PROGRAM(
+    BSDTAR_EXECUTABLE NAMES bsdtar
+    DOC "Path to the bsdtar executable"
+)
+
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(BsdTar DEFAULT_MSG BSDTAR_EXECUTABLE)
diff --git a/CMakeScripts/FindGzip.cmake b/CMakeScripts/FindGzip.cmake
new file mode 100644
--- /dev/null
+++ b/CMakeScripts/FindGzip.cmake
@@ -0,0 +1,6 @@
+FIND_PROGRAM(
+    GZIP_EXECUTABLE NAMES gzip
+    DOC "Path to the gzip executable"
+)
+
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(Gzip DEFAULT_MSG GZIP_EXECUTABLE)
diff --git a/CMakeScripts/FindSphinx.cmake b/CMakeScripts/FindSphinx.cmake
--- a/CMakeScripts/FindSphinx.cmake
+++ b/CMakeScripts/FindSphinx.cmake
@@ -1,7 +1,7 @@
 FIND_PROGRAM(
     SPHINX_EXECUTABLE
     NAMES sphinx-build sphinx-build2
-    DOC "Path to sphinx-build executable"
+    DOC "Path to the sphinx-build executable"
 )
 
 FIND_PACKAGE_HANDLE_STANDARD_ARGS(Sphinx DEFAULT_MSG SPHINX_EXECUTABLE)
diff --git a/CMakeScripts/FindVirtualenv.cmake b/CMakeScripts/FindVirtualenv.cmake
new file mode 100644
--- /dev/null
+++ b/CMakeScripts/FindVirtualenv.cmake
@@ -0,0 +1,7 @@
+FIND_PROGRAM(
+    VIRTUALENV_EXECUTABLE
+    NAMES virtualenv virtualenv2
+    DOC "Path to the virtualenv executable"
+)
+
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(Virtualenv DEFAULT_MSG VIRTUALENV_EXECUTABLE)
diff --git a/CMakeScripts/FindXz.cmake b/CMakeScripts/FindXz.cmake
new file mode 100644
--- /dev/null
+++ b/CMakeScripts/FindXz.cmake
@@ -0,0 +1,6 @@
+FIND_PROGRAM(
+    XZ_EXECUTABLE NAMES xz
+    DOC "Path to the xz executable"
+)
+
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(Xz DEFAULT_MSG XZ_EXECUTABLE)
diff --git a/CMakeScripts/LightsdVersion.cmake b/CMakeScripts/LightsdVersion.cmake
new file mode 100644
--- /dev/null
+++ b/CMakeScripts/LightsdVersion.cmake
@@ -0,0 +1,7 @@
+# NOTE: auto-generated by the release target
+SET(CPACK_PACKAGE_VERSION_MAJOR "1")
+SET(CPACK_PACKAGE_VERSION_MINOR "2")
+SET(CPACK_PACKAGE_VERSION_PATCH "0")
+SET(LIGHTSD_PRERELEASE "rc.1")
+SET(LIGHTSD_BUILD "")
+SET(LIGHTSD_VERSION "1.2.0-rc.1")
diff --git a/dist/CMakeLists.txt b/dist/CMakeLists.txt
new file mode 100644
--- /dev/null
+++ b/dist/CMakeLists.txt
@@ -0,0 +1,74 @@
+IF (
+    PYTHONINTERP_FOUND
+    AND PYTHON_VERSION_MAJOR EQUAL 3
+    AND PYTHON_VERSION_MINOR GREATER 2 # os.replace
+    AND VIRTUALENV_FOUND
+    AND HG_FOUND
+    AND BSDTAR_FOUND
+    AND GZIP_FOUND
+    AND XZ_FOUND
+)
+    MESSAGE(
+        STATUS
+        "Python >= 3.3, virtualenv, bsdtar, gzip, xz, and mercurial (hg) "
+        "found, release commands enabled"
+    )
+
+    SET(VENV_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/py-env")
+    SET(VENV_PYTHON "${VENV_DIRECTORY}/bin/python")
+    SET(VENV_STAMP "${VENV_DIRECTORY}/stamp")
+    SET(PIP_REQUIREMENTS "${CMAKE_CURRENT_SOURCE_DIR}/requirements-release.txt")
+
+    CONFIGURE_FILE(release.py.in "${CMAKE_CURRENT_BINARY_DIR}/release.py" @ONLY)
+
+    ADD_CUSTOM_COMMAND(
+        OUTPUT "${VENV_STAMP}"
+        COMMAND "${VIRTUALENV_EXECUTABLE}" -q -p "${PYTHON_EXECUTABLE}" "${VENV_DIRECTORY}"
+        COMMAND "${VENV_DIRECTORY}/bin/pip" -q install -r "${PIP_REQUIREMENTS}"
+        COMMAND "${CMAKE_COMMAND}" -E touch "${VENV_STAMP}"
+        DEPENDS "${PIP_REQUIREMENTS}"
+        COMMENT "Setting up a Python virtualenv at ${VENV_DIRECTORY} for the release script"
+        VERBATIM
+    )
+
+    ADD_CUSTOM_TARGET(
+        release
+        COMMAND "${VENV_PYTHON}" "${CMAKE_CURRENT_BINARY_DIR}/release.py" "release"
+        DEPENDS "${VENV_STAMP}"
+        VERBATIM
+    )
+
+    ADD_CUSTOM_TARGET(
+        pre_release
+        COMMAND "${VENV_PYTHON}" "${CMAKE_CURRENT_BINARY_DIR}/release.py" "pre_release"
+        DEPENDS "${VENV_STAMP}"
+        VERBATIM
+    )
+
+    ADD_CUSTOM_TARGET(
+        package_release
+        COMMAND "${VENV_PYTHON}" "${CMAKE_CURRENT_BINARY_DIR}/release.py" "package_release"
+        DEPENDS "${VENV_STAMP}"
+        VERBATIM
+    )
+
+    ADD_CUSTOM_TARGET(
+        release_new_tag
+        COMMAND "${VENV_PYTHON}" "${CMAKE_CURRENT_BINARY_DIR}/release.py" "new_tag"
+        DEPENDS "${VENV_STAMP}"
+        VERBATIM
+    )
+
+    ADD_CUSTOM_TARGET(
+        release_docs
+        COMMAND "${VENV_PYTHON}" "${CMAKE_CURRENT_BINARY_DIR}/release.py" "docs"
+        DEPENDS "${VENV_STAMP}" docs
+        VERBATIM
+    )
+ELSE ()
+    MESSAGE(
+        STATUS
+        "Python >= 3.3 and/or virtualenv, bsdtar, gzip, xz, mercurial (hg) "
+        "weren't found, release commands disabled"
+    )
+ENDIF ()
diff --git a/dist/dpkg/.hgtags b/dist/dpkg/.hgtags
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/.hgtags
@@ -0,0 +1,3 @@
+11534a873dbc92fd9c7225cf37d67942f852a67c 1.1.0
+60c3d81a6ff2305eca554c12c42d272d5f2187db 1.1.1
+bf09addc0ea974fc378f1acf48a8255e485f87d1 1.1.2
diff --git a/dist/dpkg/debian/changelog b/dist/dpkg/debian/changelog
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/debian/changelog
@@ -0,0 +1,29 @@
+lightsd (1.1.2-1) unstable; urgency=low
+
+  * New upstream release: fix LIFX LAN protocol v2 handling.
+
+ -- Louis Opter <kalessin@kalessin.fr>  Mon, 30 Nov 2015 10:38:18 +0000
+
+lightsd (1.1.1-1) unstable; urgency=low
+
+  * New upstream release: responsiveness improvements.
+
+ -- Louis Opter <kalessin@kalessin.fr>  Tue, 17 Nov 2015 10:19:02 +0000
+
+lightsd (1.1.0-2) unstable; urgency=low
+
+  * Fix parallel builds
+
+ -- Louis Opter <kalessin@kalessin.fr>  Sat, 14 Nov 2015 06:35:43 +0000
+
+lightsd (1.1.0-1) unstable; urgency=low
+
+  * First published release
+
+ -- Louis Opter <kalessin@kalessin.fr>  Sun, 08 Nov 2015 08:06:51 +0000
+
+lightsd (1.0.1-1) unstable; urgency=low
+
+  * Initial release
+
+ -- Louis Opter <kalessin@kalessin.fr>  Tue, 03 Nov 2015 07:43:24 +0000
diff --git a/dist/dpkg/debian/compat b/dist/dpkg/debian/compat
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/debian/compat
@@ -0,0 +1,1 @@
+9
diff --git a/dist/dpkg/debian/control b/dist/dpkg/debian/control
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/debian/control
@@ -0,0 +1,23 @@
+Source: lightsd
+Section: contrib/utils
+Priority: optional
+Maintainer: Louis Opter <kalessin@kalessin.fr>
+Build-Depends: debhelper (>= 9), cmake (>= 2.8.9), libevent-dev (>= 2.0.19)
+Suggests: python3, ipython3
+Standards-Version: 3.9.6
+Homepage: https://github.com/lopter/lightsd/
+Vcs-Git: https://github.com/lopter/dpkg-lightsd.git
+Vcs-Browser: https://github.com/lopter/dpkg-lightsd/
+
+Package: lightsd
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Description: Centralized daemon to control your LIFX bulbs
+ lightsd discovers LIFX bulbs on the local network and acts as a central
+ point of control for them via a JSON-RPC interface.
+ .
+ The JSON-RPC interface can run over TCP, named pipes as well as Unix
+ sockets and allows you to retrieve the state of the bulbs or set their
+ colors. Effects, grouping and de-grouping operations are also supported.
+ .
+ Visit http://lightsd.readthedocs.org/ for more informations.
diff --git a/dist/dpkg/debian/copyright b/dist/dpkg/debian/copyright
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/debian/copyright
@@ -0,0 +1,62 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: lightsd
+Source: https://github.com/lopter/lightsd
+
+Files: *
+Copyright: 2014-2015 Louis Opter <kalessin@kalessin.fr>
+License: GPL-3.0+
+
+Files: debian/*
+Copyright: 2015 Louis Opter <kalessin@kalessin.fr>
+License: BSD-3-clause
+
+Files: examples/*
+Copyright: 2015 Louis Opter <kalessin@kalessin.fr>
+License: BSD-3-clause
+
+Files: share/lightsc.sh
+Copyright: 2015 Louis Opter <kalessin@kalessin.fr>
+License: BSD-3-clause
+
+License: GPL-3.0+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
+
+License: BSD-3-clause
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ .
+ 1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ .
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ .
+ 3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
diff --git a/dist/dpkg/debian/docs b/dist/dpkg/debian/docs
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/debian/docs
@@ -0,0 +1,1 @@
+README.rst
diff --git a/dist/dpkg/debian/init.d b/dist/dpkg/debian/init.d
new file mode 100755
--- /dev/null
+++ b/dist/dpkg/debian/init.d
@@ -0,0 +1,129 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides:          lightsd
+# Required-Start:    $local_fs $network $syslog $remote_fs
+# Required-Stop:     $local_fs $network $syslog $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Start lightsd at boot time
+# Description:       Centralized daemon to discover and control LIFX bulbs.
+### END INIT INFO
+
+# Author: Louis Opter <kalessin@kalessin.fr>
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="lightsd"
+NAME=lightsd
+DAEMON=/usr/bin/lightsd
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+DAEMON_ARGS="
+    --daemonize
+    --verbosity warning
+    --user lightsd
+    --pidfile $PIDFILE
+    --socket /var/run/lightsd/socket
+    --command-pipe /var/run/lightsd/pipe
+    $DAEMON_OPTS
+"
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+do_start()
+{
+	# Return
+	#   0 if daemon has been started
+	#   1 if daemon was already running
+	#   2 if daemon could not be started
+	start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
+		|| return 1
+	start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- $DAEMON_ARGS \
+		|| return 2
+}
+
+do_stop()
+{
+	# Return
+	#   0 if daemon has been stopped
+	#   1 if daemon was already stopped
+	#   2 if daemon could not be stopped
+	#   other if a failure occurred
+	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
+	RETVAL="$?"
+	[ "$RETVAL" = 2 ] && return 2
+	# Wait for children to finish too if this is a daemon that forks
+	# and if the daemon is only ever run from this initscript.
+	# If the above conditions are not satisfied then add some other code
+	# that waits for the process to drop all resources that could be
+	# needed by services started subsequently.  A last resort is to
+	# sleep for some time.
+	start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
+	[ "$?" = 2 ] && return 2
+	# Many daemons don't delete their pidfiles when they exit.
+	rm -f $PIDFILE
+	return "$RETVAL"
+}
+
+case "$1" in
+  start)
+	[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+	do_start
+	case "$?" in
+		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+	esac
+	;;
+  stop)
+	[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+	do_stop
+	case "$?" in
+		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+	esac
+	;;
+  status)
+	status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+	;;
+  restart|force-reload)
+	log_daemon_msg "Restarting $DESC" "$NAME"
+	do_stop
+	case "$?" in
+	  0|1)
+		do_start
+		case "$?" in
+			0) log_end_msg 0 ;;
+			1) log_end_msg 1 ;; # Old process is still running
+			*) log_end_msg 1 ;; # Failed to start
+		esac
+		;;
+	  *)
+		# Failed to stop
+		log_end_msg 1
+		;;
+	esac
+	;;
+  *)
+	echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+	exit 3
+	;;
+esac
+
+:
+
+# vim: set ft=sh:
diff --git a/dist/dpkg/debian/lightsd.default b/dist/dpkg/debian/lightsd.default
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/debian/lightsd.default
@@ -0,0 +1,10 @@
+# Defaults for lightsd initscript
+# sourced by /etc/init.d/lightsd
+# installed at /etc/default/lightsd by the maintainer scripts
+
+#
+# This is a POSIX shell fragment
+#
+
+# Additional options that are passed to the Daemon.
+DAEMON_OPTS=""
diff --git a/dist/dpkg/debian/postinst b/dist/dpkg/debian/postinst
new file mode 100755
--- /dev/null
+++ b/dist/dpkg/debian/postinst
@@ -0,0 +1,55 @@
+#!/bin/sh
+# postinst script for lightsd
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <postinst> `configure' <most-recently-configured-version>
+#        * <old-postinst> `abort-upgrade' <new version>
+#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+#          <new-version>
+#        * <postinst> `abort-remove'
+#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+#          <failed-install-package> <version> `removing'
+#          <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+    configure)
+        getent group lightsd >/dev/null || groupadd -r lightsd
+        getent passwd lightsd >/dev/null || useradd -r -d / -g lightsd lightsd
+
+        cat << 'EOF'
+
+lightsd runs under the `lightsd' user and group by default; add yourself to
+this group to be able to open lightsd's socket and pipe under /var/run/lightsd:
+
+  gpasswd -a $USER lightsd
+
+Re-open your current desktop or ssh session for the change to take effect.
+Then use systemctl to start lightsd; you can start playing with lightsd with:
+
+  `lightsd --prefix`/share/doc/lightsd/examples/lightsc.py
+
+EOF
+    ;;
+
+    abort-upgrade|abort-remove|abort-deconfigure)
+    ;;
+
+    *)
+        echo "postinst called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/dist/dpkg/debian/postrm b/dist/dpkg/debian/postrm
new file mode 100755
--- /dev/null
+++ b/dist/dpkg/debian/postrm
@@ -0,0 +1,41 @@
+#!/bin/sh
+# postrm script for lightsd
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <postrm> `remove'
+#        * <postrm> `purge'
+#        * <old-postrm> `upgrade' <new-version>
+#        * <new-postrm> `failed-upgrade' <old-version>
+#        * <new-postrm> `abort-install'
+#        * <new-postrm> `abort-install' <old-version>
+#        * <new-postrm> `abort-upgrade' <old-version>
+#        * <disappearer's-postrm> `disappear' <overwriter>
+#          <overwriter-version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+    purge|remove|abort-install)
+        getent passwd lightsd >/dev/null && userdel lightsd
+    ;;
+
+    upgrade|failed-upgrade|abort-upgrade|disappear)
+    ;;
+
+    *)
+        echo "postrm called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/dist/dpkg/debian/rules b/dist/dpkg/debian/rules
new file mode 100755
--- /dev/null
+++ b/dist/dpkg/debian/rules
@@ -0,0 +1,48 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+DH_VERBOSE = 1
+
+# see EXAMPLES in dpkg-buildflags(1) and read /usr/share/dpkg/*
+DPKG_EXPORT_BUILDFLAGS = 1
+include /usr/share/dpkg/default.mk
+
+# see FEATURE AREAS in dpkg-buildflags(1)
+export DEB_BUILD_MAINT_OPTIONS = hardening=+all
+
+# see ENVIRONMENT in dpkg-buildflags(1)
+# package maintainers to append CFLAGS
+#export DEB_CFLAGS_MAINT_APPEND  = -Wall -pedantic
+# package maintainers to append LDFLAGS
+#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
+
+ifneq (,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
+    NUMJOBS = $(patsubst parallel=%,%,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
+    MAKEFLAGS += -j$(NUMJOBS)
+endif
+
+ifeq (,$(wildcard /run))
+    RUNTIME_DIRECTORY=/run
+else
+    RUNTIME_DIRECTORY=/var/run
+endif
+
+# main packaging script based on dh7 syntax
+%:
+	dh $@ 
+
+# debmake generated override targets
+# This is example for Cmake (See http://bugs.debian.org/641051 )
+override_dh_auto_configure:
+	dh_auto_configure --					\
+	    	-DCMAKE_BUILD_TYPE=RELEASE			\
+		-DCMAKE_INSTALL_PREFIX=/usr			\
+		-DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH)	\
+		-DLGTD_RUNTIME_DIRECTORY=$(RUNTIME_DIRECTORY)/lightsd
+
+override_dh_auto_build:
+	dh_auto_build --parallel
+
+override_dh_auto_install:
+	dh_auto_install
+	rm -f debian/lightsd/usr/share/doc/lightsd/COPYING
diff --git a/dist/dpkg/debian/source/format b/dist/dpkg/debian/source/format
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/debian/source/format
@@ -0,0 +1,1 @@
+3.0 (quilt)
diff --git a/dist/dpkg/debian/watch b/dist/dpkg/debian/watch
new file mode 100644
--- /dev/null
+++ b/dist/dpkg/debian/watch
@@ -0,0 +1,7 @@
+# See uscan(1) for format
+
+# Compulsory line, this is a version 3 file
+version=3
+
+opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/lightsd-$1\.tar\.gz/ \
+       https://github.com/lopter/lightsd/tags .*/?(\d+\.\d+.\d+)\.tar\.gz
diff --git a/dist/homebrew/LICENSE b/dist/homebrew/LICENSE
new file mode 100644
--- /dev/null
+++ b/dist/homebrew/LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of homebrew-lightsd nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/dist/homebrew/README.rst b/dist/homebrew/README.rst
new file mode 100644
--- /dev/null
+++ b/dist/homebrew/README.rst
@@ -0,0 +1,31 @@
+Homebrew lightsd
+================
+
+This `brew`_ tap let you install `lightsd`_, a daemon written C to control your
+LIFX wifi smart bulbs, on Mac OS X.
+
+.. _brew: http://brew.sh
+.. _lightsd: https://github.com/lopter/lightsd/
+
+Instructions
+------------
+
+Make sure you have brew installed: http://brew.sh.
+
+::
+
+   brew install lopter/lightsd/lightsd
+
+Or:
+
+::
+
+   brew tap lopter/lightsd
+   brew install lightsd
+
+Documentation
+-------------
+
+Please checkout lightsd's repository: https://github.com/lopter/lightsd/.
+
+.. vim: set tw=80 spelllang=en spell:
diff --git a/dist/homebrew/lightsd.rb b/dist/homebrew/lightsd.rb
new file mode 100644
--- /dev/null
+++ b/dist/homebrew/lightsd.rb
@@ -0,0 +1,99 @@
+require "formula"
+
+class Lightsd < Formula
+  desc "Daemon to control your LIFX wifi smart bulbs"
+  homepage "https://github.com/lopter/lightsd/"
+  url "{{ archive_url  }}"
+  sha256 "{{ archive_sha256 }}"
+  revision 1
+
+  depends_on "cmake" => :build
+  depends_on "libevent" => :build
+  depends_on "python3" => :optional
+
+  def install
+    # XXX, wtf? https://github.com/Homebrew/homebrew/issues/46061
+    ENV["PATH"] = "/usr/bin:#{ENV["PATH"]}"
+
+    args = std_cmake_args
+    args << "-DLGTD_RUNTIME_DIRECTORY=#{var}/run/lightsd"
+
+    # idk what std_cmake_args is supposed to do but it appears to be missing
+    # proper release flags:
+    cflags = %W[
+      -fstack-protector-strong
+      --param=ssp-buffer-size=4
+      -O3
+      -DNDEBUG
+    ]
+    args << "-DCMAKE_BUILD_TYPE=RELEASE"
+    args << "-DCMAKE_C_FLAGS_RELEASE='#{cflags * " "}'"
+
+    system "cmake", *args
+    system "make", "install"
+  end
+
+  def plist; <<-EOS.undent
+    <?xml version="1.0" encoding="UTF-8"?>
+    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+    <plist version="1.0">
+      <dict>
+        <key>KeepAlive</key>
+        <dict>
+          <key>SuccessfulExit</key>
+          <false/>
+        </dict>
+        <key>Label</key>
+        <string>#{plist_name}</string>
+        <key>ProgramArguments</key>
+        <array>
+          <string>#{opt_bin}/lightsd</string>
+          <string>-v</string>
+          <string>warning</string>
+          <string>-s</string>
+          <string>#{var}/run/lightsd/socket</string>
+          <string>-c</string>
+          <string>#{var}/run/lightsd/pipe</string>
+        </array>
+        <key>RunAtLoad</key>
+        <true/>
+        <key>WorkingDirectory</key>
+        <string>#{var}</string>
+        <key>StandardErrorPath</key>
+        <string>#{var}/log/lightsd.log</string>
+        <key>StandardOutPath</key>
+        <string>#{var}/log/lightsd.log</string>
+      </dict>
+    </plist>
+    EOS
+  end
+
+  def caveats; <<-EOS.undent
+    Once you've started lightsd with launchctl load (see below), you can start
+    poking around with lightsc.py:
+
+      `lightsd --prefix`/share/lightsd/examples/lightsc.py
+    EOS
+  end
+
+  head do
+    url "https://github.com/lopter/lightsd.git"
+  end
+
+  devel do
+    url "file:///Users/louis/projs/lightsd", :using => :hg
+    version "{{ dev_version }}"
+  end
+
+  test do
+    Dir.mktmpdir("lightsd-test") do |dir|
+      args = %W[
+        -l ::1:0
+        -l 127.0.0.1:0
+        -c #{dir}/lightsd.cmd
+        -h
+      ]
+      system "#{bin}/lightsd", *args
+    end
+  end
+end
diff --git a/dist/openwrt/utils/lightsd/Makefile b/dist/openwrt/utils/lightsd/Makefile
new file mode 100644
--- /dev/null
+++ b/dist/openwrt/utils/lightsd/Makefile
@@ -0,0 +1,62 @@
+#
+# Copyright (C) 2015 Louis Opter <kalessin@kalessin.fr>
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=lightsd
+PKG_VERSION:={{ version }}
+PKG_RELEASE:=1
+PKG_MAINTAINER:=Louis Opter <kalessin@kalessin.fr>
+PKG_LICENSE:=GPL-3.0+
+PKG_SOURCE_URL:=https://downloads.lightsd.io/releases/
+#PKG_SOURCE_URL:=http://localhost:8000/
+PKG_SOURCE:={{ archive_name }}
+PKG_MD5SUM:={{ archive_md5 }}
+PKG_BUILD_DIR:=$(BUILD_DIR)/lightsd-$(PKG_VERSION)
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/cmake.mk
+
+CMAKE_OPTIONS += \
+	-DCMAKE_BUILD_TYPE=RELEASE \
+	-DLGTD_RUNTIME_DIRECTORY=/var/run/lightsd
+
+define Package/lightsd
+  SECTION:=utils
+  CATEGORY:=Utilities
+  DEPENDS:=+libevent2-core
+  TITLE:=Daemon to control your LIFX Wi-Fi smart bulbs
+  MAINTAINER:=Louis Opter <kalessin@kalessin.fr>
+  URL:=https://github.com/lopter/lightsd
+  USERID:=lightsd:lightsd
+endef
+
+define Package/lightsd/install
+	$(INSTALL_DIR) \
+		$(1)/etc/init.d/ \
+		$(1)/usr/bin \
+		$(1)/usr/share/lightsd \
+		$(1)/usr/share/doc/lightsd
+
+	$(INSTALL_BIN) \
+		$(PKG_INSTALL_DIR)/usr/bin/lightsd \
+		$(1)/usr/bin/lightsd
+
+	$(INSTALL_BIN) \
+		./files/lightsd.init \
+		$(1)/etc/init.d/lightsd
+
+	$(CP) \
+		$(PKG_INSTALL_DIR)/usr/share/lightsd \
+		$(1)/usr/share/lightsd
+
+	$(CP) \
+		$(PKG_INSTALL_DIR)/usr/share/doc/lightsd \
+		$(1)/usr/share/doc/lightsd
+endef
+
+$(eval $(call BuildPackage,lightsd))
diff --git a/dist/openwrt/utils/lightsd/files/lightsd.init b/dist/openwrt/utils/lightsd/files/lightsd.init
new file mode 100644
--- /dev/null
+++ b/dist/openwrt/utils/lightsd/files/lightsd.init
@@ -0,0 +1,19 @@
+#!/bin/sh /etc/rc.common
+# Copyright (C) 2015 Louis Opter <kalessin@kalessin.fr>
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+
+START=60
+
+USE_PROCD=1
+
+lightsd=/usr/bin/lightsd
+rundir=`$lightsd --rundir`
+
+start_service() {
+	procd_open_instance
+	procd_set_param command $lightsd -S -v warning -t -u lightsd -s "$rundir/socket" -c "$rundir/pipe"
+        procd_set_param respawn
+	procd_close_instance
+}
diff --git a/dist/pkgbuild/.SRCINFO b/dist/pkgbuild/.SRCINFO
new file mode 100644
--- /dev/null
+++ b/dist/pkgbuild/.SRCINFO
@@ -0,0 +1,19 @@
+pkgbase = lightsd
+	pkgdesc = Daemon to control your LIFX smart bulbs via a JSON-RPC API
+	pkgver = {{ version }}
+	pkgrel = 1
+	epoch = 1
+	url = https://www.github.com/lopter/lightsd/
+	install = lightsd.install
+	arch = i686
+	arch = x86_64
+	license = GPL3
+	makedepends = cmake>=2.8.11
+	depends = libevent>=2.0.19
+	optdepends = python: to run the interactive lightsc.py example client
+	optdepends = ipython: makes lightsc.py more user-friendly
+	source = {{ archive_url }}
+	sha256sums = {{ archive_sha256 }}
+
+pkgname = lightsd
+
diff --git a/dist/pkgbuild/PKGBUILD b/dist/pkgbuild/PKGBUILD
new file mode 100644
--- /dev/null
+++ b/dist/pkgbuild/PKGBUILD
@@ -0,0 +1,44 @@
+# Maintainer: Louis Opter <kalessin@kalessin.fr>
+
+pkgname=lightsd
+pkgver={{ version }}
+pkgrel=1
+epoch=1
+pkgdesc="Daemon to control your LIFX smart bulbs via a JSON-RPC API"
+arch=("i686" "x86_64")
+url="https://www.github.com/lopter/lightsd/"
+license=("GPL3")
+depends=("libevent>=2.0.19")
+optdepends=(
+    "python: to run the interactive lightsc.py example client"
+    "ipython: makes lightsc.py more user-friendly"
+)
+makedepends=("cmake>=2.8.11")
+source=("{{ archive_url }}")
+sha256sums=("{{ archive_sha256 }}")
+#source=("src/${pkgname}-${pkgver}::hg+file:///home/kal/projs/lightsd")
+#sha256sums="SKIP"
+install=lightsd.install
+
+build() {
+    cd "$srcdir/$pkgname-$pkgver"
+
+    cmake .                         \
+        -DCMAKE_BUILD_TYPE=RELEASE  \
+        -DCMAKE_INSTALL_PREFIX=/usr \
+        -DLGTD_RUNTIME_DIRECTORY=/run/lightsd
+
+    make
+}
+
+check() {
+    cd "$srcdir/$pkgname-$pkgver"
+
+    make test
+}
+
+package() {
+    cd "$srcdir/$pkgname-$pkgver"
+
+    make DESTDIR="$pkgdir/" install
+}
diff --git a/dist/pkgbuild/lightsd.install b/dist/pkgbuild/lightsd.install
new file mode 100644
--- /dev/null
+++ b/dist/pkgbuild/lightsd.install
@@ -0,0 +1,22 @@
+post_install() {
+    getent group lightsd >/dev/null || groupadd -r lightsd
+    getent passwd lightsd >/dev/null || useradd -r -d / -g lightsd lightsd
+
+    cat << 'EOF'
+
+lightsd runs under the `lightsd' user and group by default; add yourself to
+this group to be able to open lightsd's socket and pipe under /run/lightsd:
+
+  gpasswd -a $USER lightsd
+
+Re-open your current desktop or ssh session for the change to take effect.
+Then use systemctl to start lightsd; you can start playing with lightsd with:
+
+  `lightsd --prefix`/share/doc/lightsd/examples/lightsc.py
+
+EOF
+}
+
+post_remove() {
+    getent passwd lightsd >/dev/null && userdel lightsd
+}
diff --git a/dist/release.py.in b/dist/release.py.in
new file mode 100644
--- /dev/null
+++ b/dist/release.py.in
@@ -0,0 +1,438 @@
+#!/usr/bin/env python3
+
+import click
+import datetime
+import functools
+import hashlib
+import os
+import itertools
+import jinja2
+import locale
+import pytz
+import requests
+import semver
+import subprocess
+import sys
+
+LIGHTSD_VERSION = "@LIGHTSD_VERSION@"
+# where the lightsd sources are:
+LIGHTSD_SOURCE_DIR = "@LIGHTSD_SOURCE_DIR@"
+# where the build is:
+LIGHTSD_BINARY_DIR = "@LIGHTSD_BINARY_DIR@"
+# where all the downstream repositories are, they will be pushed by by buildbot
+# (which manage the credentials to those repositories)
+LIGHTSD_PKGS_DIR = "@LIGHTSD_RELEASE_PACKAGES_DIR@"
+# where to put the generated archives served at
+# https://downloads.lightsd.io/releases/:
+LIGHTSD_ARCHIVES_DIR = "@LIGHTSD_RELEASE_ARCHIVES_DIR@"
+# where to manage the documentation served at https://docs.lighsd.io/:
+LIGHTSD_DOCS_DIR = "@LIGHTSD_RELEASE_DOCS_DIR@"
+
+# paths to utilities:
+HG_EXECUTABLE = "@HG_EXECUTABLE@"
+BSDTAR_EXECUTABLE = "@BSDTAR_EXECUTABLE@"
+GZIP_EXECUTABLE = "@GZIP_EXECUTABLE@"
+XZ_EXECUTABLE = "@XZ_EXECUTABLE@"
+
+COMMIT_AUTHOR = "Buildbot <builbot@kalessin.fr>"
+
+FILE_READ_SIZE = 32768
+
+TARBALL_COMPRESSORS = [
+    # suffix, path, extra_flags
+    ("gz", GZIP_EXECUTABLE, ["--best"]),
+    ("xz", XZ_EXECUTABLE, []),
+]
+
+prereq_echo = functools.partial(click.secho, fg="magenta", bold=True)
+action_echo = functools.partial(click.secho, fg="blue", bold=True)
+result_echo = functools.partial(click.secho, fg="green", bold=True)
+error_echo = functools.partial(click.secho, bold=True)
+
+
+def repopath(repo):
+    return os.path.join(LIGHTSD_PKGS_DIR, repo)
+
+
+def next_dev_version(version):
+    parts, version = version, semver.format_version(**version)
+    if parts["prerelease"] is not None:
+        return semver.bump_prerelease(version)
+    return semver.parse(semver.bump_prerelease(semver.bump_patch(version)))
+
+
+def next_pre_release(version):
+    version = semver.format_version(**version)
+    return semver.parse(semver.bump_prerelease(version))
+
+
+def next_build_release(version):
+    version = semver.format_version(**version)
+    return semver.parse(semver.bump_build(version))
+
+
+def readfile(fp, read_size=FILE_READ_SIZE):
+    chunk = fp.read(read_size)
+    while chunk:
+        yield chunk
+        chunk = fp.read(read_size)
+
+
+class PackageContext:
+
+    __slots__ = (
+        "version",
+        "dev_version",
+        "archive_name",
+        "archive_md5",
+        "archive_sha256",
+        "archive_url",
+    )
+
+    def __init__(self, **kwargs):
+        for name, value in kwargs.items():
+            if name not in self.__slots__:
+                raise TypeError("{}: invalid argument {}".format(
+                    self.__class__.__name__, name
+                ))
+            setattr(self, name, value)
+
+    def as_dict(self):
+        return {name: getattr(self, name) for name in self.__slots__}
+
+
+class DownstreamPackage:
+
+    TYPE = None
+    TEMPLATES = None
+
+    def __init__(self, repo):
+        self._repo = repo
+        self._render_ctx = jinja2.Environment(loader=jinja2.FileSystemLoader(
+            os.path.join(LIGHTSD_SOURCE_DIR, "dist", self.TYPE)
+        ))
+
+    def render(self, pkg_ctx):
+        if self.TEMPLATES is None:
+            return
+
+        for filename in self.TEMPLATES:
+            template = self._render_ctx.get_template(filename)
+            with open(os.path.join(self._repo, filename), "wb") as fp:
+                template.stream(pkg_ctx.as_dict()).dump(fp, "utf-8")
+
+        # TODO: copy the other files too.
+
+    def _pre_commit(self, msg):
+        pass
+
+    def commit(self, msg):
+        self._pre_commit(msg)
+
+        dirty = bool(len(subprocess.check_output([
+            HG_EXECUTABLE, "-R", self._repo, "status"
+        ])))
+        if dirty:
+            subprocess.check_call([
+                HG_EXECUTABLE, "-R", self._repo, "commit", "-m", msg
+            ])
+        return dirty
+
+
+class DebianPackage(DownstreamPackage):
+
+    TYPE = "dpkg"
+
+    _CHANGELOG_ENTRY_FORMAT = (
+"""lightsd ({version}-1) unstable; urgency=low
+
+  * {msg}
+
+ -- {author}  {date}
+
+"""  # noqa
+    )
+
+    def render(self, pkg_ctx):
+        self._version = pkg_ctx.version
+
+    def _pre_commit(self, msg):
+        changelog_path = os.path.join(self._repo, "debian", "changelog")
+        staging_changelog_path = "-".join([changelog_path, "next"])
+        user_date_locale = locale.getlocale(locale.LC_TIME)
+        utcnow = datetime.datetime.now(pytz.utc)
+
+        # prepare new changelog:
+        with open(staging_changelog_path, "wb") as ofp:
+            try:
+                locale.setlocale(locale.LC_TIME, "C")
+                new_entry = self._CHANGELOG_ENTRY_FORMAT.format(
+                    version=self._version,
+                    msg=msg,
+                    author=COMMIT_AUTHOR,
+                    date=utcnow.strftime("%a, %d %b %Y %H:%M:%S %z")
+                ).encode("utf-8")
+                ofp.write(new_entry)
+                with open(changelog_path, "rb") as ifp:
+                    prev_entry = next(ifp)  # get the first line
+                    if new_entry.startswith(prev_entry):
+                        os.unlink(staging_changelog_path)
+                        return  # we already released that version.
+                    ofp.write(prev_entry)
+                    for chunk in readfile(ifp):
+                        ofp.write(chunk)
+            except Exception:
+                os.unlink(staging_changelog_path)
+                raise
+            finally:
+                user_date_locale = locale.setlocale(
+                    locale.LC_TIME, user_date_locale
+                )
+
+        # and replace the old one:
+        os.replace(staging_changelog_path, changelog_path)
+
+
+class HomebrewPackage(DownstreamPackage):
+
+    TYPE = "homebrew"
+    TEMPLATES = ("lightsd.rb",)
+
+
+class OpenWRTPackage(DownstreamPackage):
+
+    TYPE = "openwrt"
+    TEMPLATES = ("utils/lightsd/Makefile",)
+
+
+class PKGBUILDPackage(DownstreamPackage):
+
+    TYPE = "pkgbuild"
+    TEMPLATES = ("PKGBUILD", ".SRCINFO")
+
+
+PACKAGES = (
+    DebianPackage(repopath("dpkg-lightsd")),
+    HomebrewPackage(repopath("homebrew-lightsd")),
+    OpenWRTPackage(repopath("openwrt-lightsd")),
+    PKGBUILDPackage(repopath("pkgbuild-lightsd")),
+)
+
+LIGHTSD_VERSION_CMAKE_TEMPLATE = (
+"""# NOTE: auto-generated by the release target
+SET(CPACK_PACKAGE_VERSION_MAJOR "{major}")
+SET(CPACK_PACKAGE_VERSION_MINOR "{minor}")
+SET(CPACK_PACKAGE_VERSION_PATCH "{patch}")
+SET(LIGHTSD_PRERELEASE "{prerelease}")
+SET(LIGHTSD_BUILD "{build}")
+SET(LIGHTSD_VERSION "{version}")
+"""  # noqa
+)
+
+
+def _update_lightsd_version_cmake_include(version):
+    version_parts = {
+        k: v if v is not None else ""
+        for k, v in semver.parse(version).items()
+    }
+    lightsd_version_file = os.path.join(
+        LIGHTSD_SOURCE_DIR, "CMakeScripts", "LightsdVersion.cmake"
+    )
+    with open(lightsd_version_file, "wb") as fp:
+        fp.write(LIGHTSD_VERSION_CMAKE_TEMPLATE.format(
+            version=version, **version_parts
+        ).encode("utf-8"))
+
+
+def _release(version, next_version):
+    revision, branch = [
+        part.strip() for part in subprocess.check_output([
+            HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR, "id", "--id", "--branch"
+        ]).split()
+    ]
+    qapplied = bool(len(subprocess.check_output([
+        HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR,
+        "--config", "extensions.mq=", "qapplied"
+    ])))
+    if qapplied or revision.endswith("+") or branch != "default":
+        error_echo(
+            "Can't do a release over a dirty repository! "
+            "(rev={}, branch={}, patched={})".format(
+                revision.decode("utf-8"), branch.decode("utf-8"), qapplied
+            )
+        )
+        sys.exit(1)
+
+    # Re-gen LightsdVersion.cmake with major.minor.patch, tag, and re-gen
+    # LightsdVersion.cmake after bumping minor adding a prerelease tag:
+    version = click.prompt(
+        "Confirm the version to release",
+        default=version,
+        type=lambda arg: semver.format_version(**semver.parse(arg))
+    )
+    _update_lightsd_version_cmake_include(version)
+    subprocess.check_call([
+        HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR,
+        "tag", "-m", "Tagging release {}".format(version), version
+        # TODO: use docutils to extract the changelog section
+    ])
+    _update_lightsd_version_cmake_include(next_version)
+    subprocess.check_call([
+        HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR,
+        "commit", "-m", "Back to development, {}".format(next_version)
+    ])
+
+    subprocess.check_call([HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR, "out"])
+    if click.confirm("Are you ready to push those commit?"):
+        return
+        subprocess.check_call([HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR, "push"])
+
+
+@click.group()
+def cli():
+    pass
+
+
+@cli.command()
+def pre_release():
+    version = semver.parse(LIGHTSD_VERSION)
+    if version["prerelease"] is None:
+        version = next_pre_release(version)
+    _release(
+        semver.format_version(**version),
+        semver.format_version(**next_pre_release(version)),
+    )
+
+
+@cli.command()
+def release():
+    version = semver.parse(LIGHTSD_VERSION)
+    version["prerelease"] = version["build"] = None
+    _release(
+        semver.format_version(**version),
+        semver.format_version(**next_dev_version(version)),
+    )
+
+
+@cli.command()
+def package_release(release):
+    version = semver.parse(LIGHTSD_VERSION)
+    # TODO: version = previous_release (including build metadata)
+    #       (maybe downloads.lightsd.io can help)
+    version = next_build_release(version)
+    _release(
+        semver.format_version(**version),
+        semver.format_version(**next_build_release(version)),
+    )
+
+
+@cli.command()
+def new_tag():
+    if not LIGHTSD_PKGS_DIR or not LIGHTSD_ARCHIVES_DIR:
+        error_echo(
+            "Please configure the project with LIGHTSD_RELEASE_PACKAGES_DIR "
+            "and LIGHTSD_RELEASE_ARCHIVES_DIR to use this command."
+        )
+        sys.exit(1)
+
+    archive_name = "lightsd-{}.tar".format(LIGHTSD_VERSION)
+    archive = os.path.join(LIGHTSD_BINARY_DIR, "dist", archive_name)
+    gz_archive_name = ".".join([archive_name, "gz"])
+    gz_archive = os.path.join(LIGHTSD_ARCHIVES_DIR, gz_archive_name)
+    gz_archive_url = "https://downloads.lightsd.io/releases/{}".format(
+        gz_archive_name
+    )
+
+    action_echo("Checking for an existing release")
+    response = requests.head(gz_archive_url, allow_redirects=True)
+    if response.status_code == requests.codes.ok:
+        error_echo("Release already found at {}!".format(gz_archive_url))
+
+    prereq_echo("Cleaning-up the source tree")
+    subprocess.check_call([
+        HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR,
+        "--config", "extensions.purge=", "purge", "--abort-on-err", "--all"
+    ])
+
+    # NOTE: I wanted to use hg export but then dpkg gave me troubles because
+    #       the archive had extra files or something:
+    action_echo("Tarballing the sources into {}".format(archive))
+    if not os.path.exists(archive):
+        subprocess.check_call([
+            BSDTAR_EXECUTABLE,
+            "-C", LIGHTSD_SOURCE_DIR,
+            "-cf", archive,
+            "-s", "/^\\./{}/".format(os.path.basename(archive)),
+            "--exclude", ".hg",
+            "."
+        ])
+    os.makedirs(LIGHTSD_ARCHIVES_DIR, exist_ok=True)
+    for suffix, bin, extra_flags in TARBALL_COMPRESSORS:
+        dest = ".".join([
+            os.path.join(LIGHTSD_ARCHIVES_DIR, archive_name), suffix
+        ])
+        if os.path.exists(dest):
+            click.echo("[=] compressing with {}".format(suffix))
+            continue
+        cmd = itertools.chain.from_iterable([
+            [bin, "--stdout"], extra_flags, [archive]
+        ])
+        click.echo("[+] compressing with {}".format(suffix))
+        with open(dest, "wb") as fp:
+            subprocess.check_call(cmd, stdout=fp)
+
+    action_echo("Computing MD5 and SHA256 checksums for {}".format(gz_archive))
+    gz_archive_md5 = hashlib.md5()
+    gz_archive_sha256 = hashlib.sha256()
+    with open(gz_archive, "rb") as fp:
+        for chunk in readfile(fp):
+            gz_archive_md5.update(chunk)
+            gz_archive_sha256.update(chunk)
+    gz_archive_md5 = gz_archive_md5.hexdigest()
+    gz_archive_sha256 = gz_archive_sha256.hexdigest()
+    click.echo("[+] MD5 {}".format(gz_archive_md5))
+    click.echo("[+] SHA256 {}".format(gz_archive_sha256))
+
+    pkg_ctx = PackageContext(
+        version=LIGHTSD_VERSION,
+        dev_version=next_dev_version(semver.parse(LIGHTSD_VERSION)),
+        archive_name=gz_archive_name,
+        archive_md5=gz_archive_md5,
+        archive_sha256=gz_archive_sha256,
+        archive_url=gz_archive_url,
+    )
+    action_echo("Updating packages")
+    for pkg in PACKAGES:
+        dirty = pkg.render(pkg_ctx)
+        if not dirty:
+            click.echo("[=] {} package".format(pkg.TYPE))
+            continue
+        click.echo("[+] {} package".format(pkg.TYPE))
+        pkg.commit("New upstream release {}".format(LIGHTSD_VERSION))
+
+    result_echo("New upstream release {}".format(LIGHTSD_VERSION))
+
+
+@cli.command()
+def docs():
+    version = semver.parse(LIGHTSD_VERSION)
+    dest_dir = os.path.join(LIGHTSD_DOCS_DIR, LIGHTSD_VERSION)
+    docs_dir = os.path.join(LIGHTSD_BINARY_DIR, "docs", "_build")
+    os.makedirs(dest_dir, exist_ok=True)
+    if version["prerelease"] is None and version["build"] is None:
+        alias = "current"
+    else:
+        alias = "latest"
+    action_echo("Copying files into {}".format(dest_dir))
+    subprocess.check_call([
+        "cp", "-a", docs_dir + os.path.sep, dest_dir + os.path.sep
+    ])
+    action_echo("Updating alias {}".format(alias))
+    subprocess.check_call([
+        "ln", "-snf", dest_dir, os.path.join(LIGHTSD_DOCS_DIR, alias)
+    ])
+    result_echo("{} ({}) updated".format(dest_dir, alias))
+
+
+if __name__ == "__main__":
+    cli()
diff --git a/dist/requirements-release.txt b/dist/requirements-release.txt
new file mode 100644
--- /dev/null
+++ b/dist/requirements-release.txt
@@ -0,0 +1,6 @@
+click>=6.6,<7.0
+jinja2>=2.8,<3.0
+requests>=2.9.1,<3.0
+semver>=2.4.1,<3.0.0
+
+pytz