Add auto release module
Added the RELEASE_NOTES and auto-release script
Change-Id: Ia851fc4c447c5eaa35335c5990b86136bb769978
diff --git a/.waf-tools/osx-frameworks.py b/.waf-tools/osx-frameworks.py
index 9936778..6a473c0 100644
--- a/.waf-tools/osx-frameworks.py
+++ b/.waf-tools/osx-frameworks.py
@@ -45,7 +45,7 @@
# Download to local path and retry
Logs.info ("Sparkle framework not found, trying to download it to 'build/'")
- urllib.urlretrieve ("https://github.com/sparkle-project/Sparkle/releases/download/1.16.0/Sparkle-1.16.0.tar.bz2", "build/Sparkle.tar.bz2")
+ urllib.urlretrieve ("https://github.com/sparkle-project/Sparkle/releases/download/1.17.0/Sparkle-1.17.0.tar.bz2", "build/Sparkle.tar.bz2")
if os.path.exists('build/Sparkle.tar.bz2'):
try:
subprocess.check_call(['mkdir', 'build/Sparkle'])
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
new file mode 100644
index 0000000..28f8344
--- /dev/null
+++ b/RELEASE_NOTES.md
@@ -0,0 +1,10 @@
+Release Notes
+==============
+
+## Version 0.1.0
+
+Initial release of ChronoShare, featuring:
+
+- Decentralised file sharing
+
+- NDN-JS interface for versioning history browsing and checking out old version
\ No newline at end of file
diff --git a/auto-release.sh b/auto-release.sh
new file mode 100755
index 0000000..1fe446a
--- /dev/null
+++ b/auto-release.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+
+#######################################
+## Attention:
+## Please make sure to update your wscript, RELEASE_NOTES.md and intro.md in
+## docs beforehand, and then run this script to auto-release application.
+
+usage()
+{
+ echo "Usage:"
+ echo " $0 <VERSION> <IDENTITY> <SPARKLE_KEY>"
+ echo ""
+ echo "Options:"
+ echo " <VERSION>: the version that will be used for this release"
+ echo " <IDENTITY>: XCode identity (Mac Developer) that will be used for signing the application"
+ echo " <SPARKLE_KEY>: the path to your Sparkle private key for signing the application"
+ echo ""
+ exit
+}
+
+#######################################
+## Script for automatically release application binary
+
+VERSION=${VERSION:-$1}
+IDENTITY=${IDENTITY:-$2}
+KEY_LOCATION=${KEY_LOCATION:-$3}
+
+if [[ -z $VERSION ]] || [[ -z $IDENTITY ]] || [[ -z $KEY_LOCATION ]]; then
+ usage
+fi
+
+echo "Preparing release $VERSION"
+echo " will sign with XCode identity: $IDENTITY"
+echo " will sign with Sparkle key: $KEY_LOCATION"
+
+BINARY_WEBSERVER=${BINARY_WEBSERVER:-named-data.net:binaries/ChronoShare/}
+
+rm -rf build/release
+mkdir build/release
+
+#######################################
+## Build .dmg file with code sign with Apple Developer ID
+
+echo "[auto-release] Build .dmg file and sign with Apple Developer ID"
+
+./make-osx-bundle.py -r "${VERSION}" --codesign="${IDENTITY}"
+
+#######################################
+## Code sign with Sparkle (private key existed)
+
+cp build/ChronoShare-${VERSION}.dmg build/release/
+cp build/release-notes-${VERSION}.html build/release/
+
+#######################################
+## Code sign with Sparkle (private key needed)
+
+#OPENSSL="/usr/bin/openssl"
+#openssl gendsa <($OPENSSL dsaparam 4096) -out dsa_priv.pem
+#chmod 0400 dsa_priv.pem
+#openssl dsa -in dsa_priv.pem -pubout -out ndn_sparkle_pub.pem
+#mv ndn_sparkle_pub.pem ../res/
+#./bin/sign_update "../NDN-${VERSION}.dmg" "${KEY_LOCATION}"
+
+#######################################
+## Generate appcast xml file
+
+echo "[auto-release] Generate appcast xml file"
+
+./build/Sparkle/bin/generate_appcast "${KEY_LOCATION}" build/release/
+
+cp sparkle-appcast.xml build/
+cat <<EOF | python -
+import xml.etree.ElementTree
+cast = xml.etree.ElementTree.parse('build/sparkle-appcast.xml')
+item = xml.etree.ElementTree.parse('build/release/sparkle-appcast.xml')
+
+channel = cast.getroot()[0]
+
+for item in item.getroot().findall('./channel/item'):
+ version = item.findall('.//title')[0].text
+ item.findall('.//title')[0].text = "Version %s" % version
+ notes = xml.etree.ElementTree.Element('ns0:releaseNotesLink')
+ notes.text = 'https://named-data.net/binaries/ChronoShare/release-notes-%s.html' % version
+ item.append(notes)
+ channel.append(item)
+
+cast.write('sparkle-appcast.xml', encoding="utf-8")
+EOF
+
+cp sparkle-appcast.xml build/release/
+
+#######################################
+## Upload dmg & xml & html to https://named-data.net/binaries/ChronoShare/
+
+echo "[auto-release] Publish dmg xml and html file to website server"
+
+pushd build
+pushd release
+ln -s "ChronoShare-${VERSION}.dmg" ChronoShare.dmg
+ln -s "release-notes-${VERSION}.html" release-notes.html
+popd
+popd
+
+echo "Ready to upload:"
+echo "rsync -avz build/release/* \"${BINARY_WEBSERVER}\""
diff --git a/docs/intro.md b/docs/intro.md
new file mode 100644
index 0000000..9eeef9d
--- /dev/null
+++ b/docs/intro.md
@@ -0,0 +1,24 @@
+ChronoShare
+===========
+
+Decentralized File Sharing Over NDN
+
+- Version controlled
+- NDN-JS interface for versioning history browsing and checking out old version
+- Dropbox like user experience (ok, their UI is fancier)
+
+## Download
+
+- macOS 10.12
+
+ * [Latest version](https://named-data.net/binaries/ChronoShare/ChronoShare.dmg)
+
+- Source
+
+ * [Github](https://github.com/named-data/ChronoShare)
+
+- Issue requests and bug reporting
+
+ * [NDN Redmine](https://redmine.named-data.net/projects/chronoshare/issues)
+
+- [Release notes](https://github.com/named-data/ChronoShare/blob/master/RELEASE_NOTES.md#release-notes)
diff --git a/make-osx-bundle.py b/make-osx-bundle.py
new file mode 100755
index 0000000..fe4e64a
--- /dev/null
+++ b/make-osx-bundle.py
@@ -0,0 +1,415 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+#
+# Loosely based on original bash-version by Sebastian Schlingmann (based, again, on a OSX application bundler
+# by Thomas Keller).
+#
+
+import sys, os, string, re, shutil, plistlib, tempfile, exceptions, datetime, tarfile
+from subprocess import Popen, PIPE
+from optparse import OptionParser
+
+os.environ['PATH'] += ":/usr/local/opt/qt5/bin:/opt/qt5/5.8/clang_64/bin"
+
+import platform
+
+if platform.system () != 'Darwin':
+ print "This script is indended to be run only on OSX platform"
+ exit (1)
+
+MIN_SUPPORTED_VERSION="10.12"
+
+current_version = tuple(int(i) for i in platform.mac_ver()[0].split('.')[0:2])
+min_supported_version = tuple(int(i) for i in MIN_SUPPORTED_VERSION.split('.')[0:2])
+
+if current_version < min_supported_version:
+ print "This script is indended to be run only on OSX >= %s platform" % MIN_SUPPORTED_VERSION
+ exit (1)
+
+options = None
+
+def gitrev():
+ return os.popen('git describe').read()[:-1]
+
+def codesign(path):
+ '''Call the codesign executable.'''
+
+ if hasattr(path, 'isalpha'):
+ path = (path,)
+
+ for p in path:
+ p = Popen(('codesign', '-vvvv', '--deep', '--force', '--sign', options.codesign, p))
+ retval = p.wait()
+ if retval != 0:
+ return retval
+ return 0
+
+class AppBundle(object):
+
+ def __init__(self, bundle, version, binary):
+ shutil.copytree (src = binary, dst = bundle, symlinks = True)
+
+ self.framework_path = ''
+ self.handled_libs = {}
+ self.bundle = bundle
+ self.version = version
+ self.infopath = os.path.join(os.path.abspath(bundle), 'Contents', 'Info.plist')
+ self.infoplist = plistlib.readPlist(self.infopath)
+ self.binary = os.path.join(os.path.abspath(bundle), 'Contents', 'MacOS', self.infoplist['CFBundleExecutable'])
+ print ' * Preparing AppBundle'
+
+ def is_system_lib(self, lib):
+ '''
+ Is the library a system library, meaning that we should not include it in our bundle?
+ '''
+ if lib.startswith('/System/Library/'):
+ return True
+ if lib.startswith('/usr/lib/'):
+ return True
+
+ return False
+
+ def is_dylib(self, lib):
+ '''
+ Is the library a dylib?
+ '''
+ return lib.endswith('.dylib')
+
+ def get_framework_base(self, fw):
+ '''
+ Extracts the base .framework bundle path from a library in an abitrary place in a framework.
+ '''
+ paths = fw.split('/')
+ for i, str in enumerate(paths):
+ if str.endswith('.framework'):
+ return '/'.join(paths[:i+1])
+ return None
+
+ def is_framework(self, lib):
+ '''
+ Is the library a framework?
+ '''
+ return bool(self.get_framework_base(lib))
+
+ def get_binary_libs(self, path):
+ '''
+ Get a list of libraries that we depend on.
+ '''
+ m = re.compile('^\t(.*)\ \(.*$')
+ libs = Popen(['otool', '-L', path], stdout=PIPE).communicate()[0]
+ libs = string.split(libs, '\n')
+ ret = []
+ bn = os.path.basename(path)
+ for line in libs:
+ g = m.match(line)
+ if g is not None:
+ lib = g.groups()[0]
+ if lib != bn:
+ ret.append(lib)
+ return ret
+
+ def handle_libs(self):
+ '''
+ Copy non-system libraries that we depend on into our bundle, and fix linker
+ paths so they are relative to our bundle.
+ '''
+ print ' * Taking care of libraries'
+
+ # Does our fwpath exist?
+ fwpath = os.path.join(os.path.abspath(self.bundle), 'Contents', 'Frameworks')
+ if not os.path.exists(fwpath):
+ os.mkdir(fwpath)
+
+ self.handle_binary_libs()
+
+ def handle_binary_libs(self, macho=None, loader_path=None):
+ '''
+ Fix up dylib depends for a specific binary.
+ '''
+ # Does our fwpath exist already? If not, create it.
+ if not self.framework_path:
+ self.framework_path = self.bundle + '/Contents/Frameworks'
+ if not os.path.exists(self.framework_path):
+ os.mkdir(self.framework_path)
+ else:
+ shutil.rmtree(self.framework_path)
+ os.mkdir(self.framework_path)
+
+ # If we weren't explicitly told which binary to operate on, pick the
+ # bundle's default executable from its property list.
+ if macho is None:
+ macho = os.path.abspath(self.binary)
+ else:
+ macho = os.path.abspath(macho)
+
+ print "Processing [%s]" % macho
+
+ libs = self.get_binary_libs(macho)
+
+ for lib in libs:
+
+ # Skip system libraries
+ if self.is_system_lib(lib):
+ continue
+
+ if 'Qt' in lib:
+ continue
+
+ # Frameworks are 'special'.
+ if self.is_framework(lib):
+ fw_path = self.get_framework_base(lib)
+ basename = os.path.basename(fw_path)
+ name = basename.split('.framework')[0]
+ rel = basename + '/' + name
+
+ abs = self.framework_path + '/' + rel
+
+ if not basename in self.handled_libs:
+ dst = self.framework_path + '/' + basename
+ print "COPY ", fw_path, dst
+ shutil.copytree(fw_path, dst, symlinks=True)
+ if name.startswith('Qt'):
+ os.remove(dst + '/' + name + '.prl')
+ os.remove(dst + '/Headers')
+ shutil.rmtree(dst + '/Versions/Current/Headers')
+
+ os.chmod(abs, 0755)
+ os.system('install_name_tool -id "@executable_path/../Frameworks/%s" "%s"' % (rel, abs))
+ self.handled_libs[basename] = True
+ self.handle_binary_libs(abs)
+
+ os.chmod(macho, 0755)
+ # print 'install_name_tool -change "%s" "@executable_path/../Frameworks/%s" "%s"' % (lib, rel, macho)
+ os.system('install_name_tool -change "%s" "@executable_path/../Frameworks/%s" "%s"' % (lib, rel, macho))
+
+ # Regular dylibs
+ else:
+ basename = os.path.basename(lib)
+ rel = basename
+
+ if not basename in self.handled_libs:
+ if lib.startswith('@loader_path'):
+ copypath = lib.replace('@loader_path', loader_path)
+ else:
+ copypath = lib
+
+ print "COPY ", copypath
+ shutil.copy(copypath, self.framework_path + '/' + basename)
+
+ abs = self.framework_path + '/' + rel
+ os.chmod(abs, 0755)
+ os.system('install_name_tool -id "@executable_path/../Frameworks/%s" "%s"' % (rel, abs))
+ self.handled_libs[basename] = True
+ self.handle_binary_libs(abs, loader_path=os.path.dirname(lib))
+
+ # print 'install_name_tool -change "%s" "@executable_path/../Frameworks/%s" "%s"' % (lib, rel, macho)
+ os.chmod(macho, 0755)
+ os.system('install_name_tool -change "%s" "@executable_path/../Frameworks/%s" "%s"' % (lib, rel, macho))
+
+ def copy_resources(self, rsrcs):
+ '''
+ Copy needed resources into our bundle.
+ '''
+ print ' * Copying needed resources'
+ rsrcpath = os.path.join(self.bundle, 'Contents', 'Resources')
+ if not os.path.exists(rsrcpath):
+ os.mkdir(rsrcpath)
+
+ # Copy resources already in the bundle
+ for rsrc in rsrcs:
+ b = os.path.basename(rsrc)
+ if os.path.isdir(rsrc):
+ shutil.copytree(rsrc, os.path.join(rsrcpath, b), symlinks=True)
+ elif os.path.isfile(rsrc):
+ shutil.copy(rsrc, os.path.join(rsrcpath, b))
+
+ return
+
+ def copy_etc(self, rsrcs):
+ '''
+ Copy needed config files into our bundle.
+ '''
+ print ' * Copying needed config files'
+ rsrcpath = os.path.join(self.bundle, 'Contents', 'etc', 'ndn')
+ if not os.path.exists(rsrcpath):
+ os.makedirs(rsrcpath)
+
+ # Copy resources already in the bundle
+ for rsrc in rsrcs:
+ b = os.path.basename(rsrc)
+ if os.path.isdir(rsrc):
+ shutil.copytree(rsrc, os.path.join(rsrcpath, b), symlinks=True)
+ elif os.path.isfile(rsrc):
+ shutil.copy(rsrc, os.path.join(rsrcpath, b))
+
+ return
+
+ def copy_framework(self, framework):
+ '''
+ Copy frameworks
+ '''
+ print ' * Copying framework'
+ rsrcpath = os.path.join(self.bundle, 'Contents', 'Frameworks', os.path.basename(framework))
+
+ shutil.copytree(framework, rsrcpath, symlinks = True)
+
+ def macdeployqt(self):
+ Popen(['macdeployqt', self.bundle, '-qmldir=src', '-executable=%s' % self.binary]).communicate()
+
+ def copy_ndn_deps(self, path):
+ '''
+ Copy over NDN dependencies (NFD and related apps)
+ '''
+ print ' * Copying NDN dependencies'
+
+ src = os.path.join(path, 'bin')
+ dst = os.path.join(self.bundle, 'Contents', 'Platform')
+ shutil.copytree(src, dst, symlinks=False)
+
+ for subdir, dirs, files in os.walk(dst):
+ for file in files:
+ abs = subdir + "/" + file
+ self.handle_binary_libs(abs)
+
+ def set_min_macosx_version(self, version):
+ '''
+ Set the minimum version of Mac OS X version that this App will run on.
+ '''
+ print ' * Setting minimum Mac OS X version to: %s' % (version)
+ self.infoplist['LSMinimumSystemVersion'] = version
+
+ def done(self):
+ plistlib.writePlist(self.infoplist, self.infopath)
+ print ' * Done!'
+ print ''
+
+class FolderObject(object):
+ class Exception(exceptions.Exception):
+ pass
+
+ def __init__(self):
+ self.tmp = tempfile.mkdtemp()
+
+ def copy(self, src, dst='/'):
+ '''
+ Copy a file or directory into foler
+ '''
+ asrc = os.path.abspath(src)
+
+ if dst[0] != '/':
+ raise self.Exception
+
+ # Determine destination
+ if dst[-1] == '/':
+ adst = os.path.abspath(self.tmp + '/' + dst + os.path.basename(src))
+ else:
+ adst = os.path.abspath(self.tmp + '/' + dst)
+
+ if os.path.isdir(asrc):
+ print ' * Copying directory: %s' % os.path.basename(asrc)
+ shutil.copytree(asrc, adst, symlinks=True)
+ elif os.path.isfile(asrc):
+ print ' * Copying file: %s' % os.path.basename(asrc)
+ shutil.copy(asrc, adst)
+
+ def symlink(self, src, dst):
+ '''
+ Create a symlink inside the folder
+ '''
+ asrc = os.path.abspath(src)
+ adst = self.tmp + '/' + dst
+ print " * Creating symlink %s" % os.path.basename(asrc)
+ os.symlink(asrc, adst)
+
+ def mkdir(self, name):
+ '''
+ Create a directory inside the folder.
+ '''
+ print ' * Creating directory %s' % os.path.basename(name)
+ adst = self.tmp + '/' + name
+ os.makedirs(adst)
+
+class DiskImage(FolderObject):
+
+ def __init__(self, filename, volname):
+ FolderObject.__init__(self)
+ print ' * Preparing to create diskimage'
+ self.filename = filename
+ self.volname = volname
+
+ def create(self):
+ '''
+ Create the disk image
+ '''
+ print ' * Creating disk image. Please wait...'
+ if os.path.exists(self.filename):
+ os.remove(self.filename)
+ shutil.rmtree(self.filename, ignore_errors=True)
+ p = Popen(['hdiutil', 'create',
+ '-srcfolder', self.tmp,
+ '-format', 'UDBZ',
+ '-volname', self.volname,
+ self.filename])
+
+ retval = p.wait()
+ print ' * Removing temporary directory.'
+ shutil.rmtree(self.tmp)
+ print ' * Done!'
+
+
+if __name__ == '__main__':
+ parser = OptionParser()
+ parser.add_option('-r', '--release', dest='release', help='Build a release. This determines the version number of the release.')
+ parser.add_option('-s', '--snapshot', dest='snapshot', help='Build a snapshot release. This determines the \'snapshot version\'.')
+ parser.add_option('-g', '--git', dest='git', help='Build a snapshot release. Use the git revision number as the \'snapshot version\'.', action='store_true', default=False)
+ parser.add_option('--no-dmg', dest='no_dmg', action='store_true', default=False, help='''Disable creation of DMG''')
+ parser.add_option('--codesign', dest='codesign', help='Identity to use for code signing. (If not set, no code signing will occur)')
+
+ options, args = parser.parse_args()
+
+ # Release
+ if options.release:
+ ver = options.release
+ # Snapshot
+ elif options.snapshot or options.git:
+ if not options.git:
+ ver = options.snapshot
+ else:
+ ver = gitrev()
+ else:
+ print 'ERROR: Neither snapshot or release selected. Bailing.'
+ parser.print_help ()
+ sys.exit(1)
+
+ # Do the finishing touches to our Application bundle before release
+ shutil.rmtree('build/dist/ChronoShare.app', ignore_errors=True)
+ a = AppBundle('build/dist/ChronoShare.app', ver, 'build/ChronoShare.app')
+ # a.copy_resources(['qt.conf'])
+ a.set_min_macosx_version('%s.0' % MIN_SUPPORTED_VERSION)
+ a.handle_binary_libs()
+ a.macdeployqt()
+ a.copy_framework("osx/Frameworks/Sparkle.framework")
+ a.done()
+
+ # Sign our binaries, etc.
+ if options.codesign:
+ print ' * Signing binaries with identity `%s\'' % options.codesign
+ codesign(a.bundle)
+ print ''
+
+ if not options.no_dmg:
+ # Create diskimage
+ title = "ChronoShare-%s" % ver
+ fn = "build/%s.dmg" % title
+ d = DiskImage(fn, title)
+ d.symlink('/Applications', '/Applications')
+ d.copy('build/dist/ChronoShare.app', '/ChronoShare.app')
+ d.create()
+
+ if options.codesign:
+ print ' * Signing .dmg with identity `%s\'' % options.codesign
+ codesign(fn)
+ print ''
+
+Popen('tail -n +3 RELEASE_NOTES.md | pandoc -f markdown -t html > build/release-notes-%s.html' % ver, shell=True).wait()
diff --git a/sparkle-appcast.xml b/sparkle-appcast.xml
index 60adc74..09ad7ba 100644
--- a/sparkle-appcast.xml
+++ b/sparkle-appcast.xml
@@ -1,4 +1,4 @@
-<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
+<rss xmlns:ns0="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>ChronoShare Changelog</title>
<link>
@@ -6,5 +6,11 @@
</link>
<description>Most recent changes with links to updates.</description>
<language>en</language>
- </channel>
+<item>
+<title>Version 0.1.0</title>
+<pubDate>Thu, 23 Mar 2017 09:32:14 -0700</pubDate>
+<ns0:minimumSystemVersion>10.12.0</ns0:minimumSystemVersion>
+<enclosure length="17713425" type="application/octet-stream" url="https://named-data.net/binaries/ChronoShare/ChronoShare-0.1.0.dmg" ns0:dsaSignature="MC4CFQCCmLhRNO6aRv1GA+CwHAl+4AQ7OwIVAI1nERVItIqvzUtyScXTiTYWML8v" ns0:shortVersionString="0.1.0" ns0:version="0.1.0" />
+<ns0:releaseNotesLink>https://named-data.net/binaries/ChronoShare/release-notes-0.1.0.html</ns0:releaseNotesLink></item>
+</channel>
</rss>