Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A number of improvements to the dev environment #291

Merged
merged 6 commits into from Nov 12, 2015

Conversation

benoit-pierre
Copy link
Member

  • fix python versions in scripts (-> python2)
  • switch setup.py to setuptools: so it's possible to create a binary wheel/egg distribution (together with the next change, Plover can even be run directly from the egg file on Linux)
  • use pkg_resources for accessing assets when available
  • rename application/plover to launch.py: because I'm lazy and don't like having to set PYTHONPATH every time I want to work on the Plover source code (this also make it possible to just create a symlink to launch.py in one of PATH directories, and run from source)

@morinted
Copy link
Member

morinted commented Nov 7, 2015

Okay, works awesome on Windows. On OS X, after building:

  • Opening crashes with error NotImplementedError: resource_filename() only supported for .egg, not .zip
  • When using the makefile as "make dist/Plover.app" (supposed to create Plover.app without packaging in a .dmg), the launch.py file gets deleted.

@morinted
Copy link
Member

morinted commented Nov 7, 2015

Okay, started by getting rid of the rm ../launch.py, added Plover.dmg to the gitignore. I'll keep playing, but if you know the solution then please enlighten me 👍

commit 155c3a63a92ff1a12b784a009443bb9152006857
Author: Ted Morin <morinted@gmail.com>
Date:   Sat Nov 7 11:55:11 2015 -0500

    Mac: don't remove launch.py, add dmg to gitignore

diff --git a/.gitignore b/.gitignore
index 5f8855b..8a696d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,6 +34,7 @@ nosetests.xml

 # mac files
 .DS_store
+Plover.dmg

 # emacs backups
 *~
diff --git a/osx/makefile b/osx/makefile
index baaa852..0bfb0db 100644
--- a/osx/makefile
+++ b/osx/makefile
@@ -6,7 +6,6 @@ all: Plover.dmg

@benoit-pierre
Copy link
Member Author

Oups, sorry about the rm, missed it. What does the OSX installation looks like?

@benoit-pierre
Copy link
Member Author

By the way, isn't Mac OS X supported by PyInstaller?

@morinted
Copy link
Member

morinted commented Nov 7, 2015

I'm not familiar with the intricacies of building Python, even more packaging into an image. You are much better with Python than I am, so I'm all ears when you talk. 😁

I've created a gist with my build output and launch crash https://gist.github.com/morinted/2a8568c3e36027e40498

If you have some crazy ideas, I'm willing to try things out.

@morinted
Copy link
Member

morinted commented Nov 7, 2015

Looks like Mac is already using setuptools. Are you available to propose a change that I can test out?

@benoit-pierre
Copy link
Member Author

What if you use PyInstaller instead, with the following spec file in osx/pyinstaller.spec:

# -*- mode: python -*-
a = Analysis(['..\\launch.py'],
             pathex=['..'],
             hiddenimports=[],
             hookspath=None)
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          Tree('..\\plover\\assets'),
          name=os.path.join('dist', 'plover'),
          debug=False,
          strip=None,
          upx=True,
          console=False,
          icon='plover.ico')
app = BUNDLE(exe,
         name='plover.app',
         icon=None,
         bundle_identifier=None)

@morinted
Copy link
Member

morinted commented Nov 7, 2015

Well, that script didn't work, but just using pyinstaller -w launch.py worked, but still on launch I get NotImplementedError: resource_filename() only supported for .egg, not .zip

@morinted
Copy link
Member

morinted commented Nov 7, 2015

Okay, got around it, how's config.py looks on my machine. If this looks right to you, then you can integrate it with the patch above and we should be good.

Also, do you use a PEP8 linter? Noticed /setup.py didn't seem to follow it at all. I'd like to try and use it PEP8 with any new code if possible. If you don't want to make the whitespace change, I can do it while merging.

# Copyright (c) 2012 Hesky Fisher
# See LICENSE.txt for details.

"""Platform dependent configuration."""

import pkg_resources
import appdirs
import os
from os.path import realpath, join, dirname, abspath, isfile, pardir
import sys


# If plover is run from a pyinstaller binary.
if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
    ASSETS_DIR = sys._MEIPASS
    PROGRAM_DIR = dirname(sys.executable)
# If plover is run from an app bundle on Mac.
elif (sys.platform.startswith('darwin') and '.app' in realpath(__file__)):
    ASSETS_DIR = os.getcwd()
    PROGRAM_DIR = abspath(join(dirname(sys.executable), *[pardir] * 3))
else:
    PROGRAM_DIR = os.getcwd()
    if pkg_resources.resource_isdir('plover', 'assets'):
        ASSETS_DIR = pkg_resources.resource_filename('plover', 'assets')
    else:
        ASSETS_DIR = join(dirname(dirname(realpath(__file__))), 'assets')


# If the program's directory has a plover.cfg file then run in "portable mode",
# i.e. store all data in the same directory. This allows keeping all Plover
# files in a portable drive.
if isfile(join(PROGRAM_DIR, 'plover.cfg')):
    CONFIG_DIR = PROGRAM_DIR
else:
    CONFIG_DIR = appdirs.user_data_dir('plover', 'plover')

@benoit-pierre
Copy link
Member Author

That version of config.py drop support for bundling Plover as an egg with PyInstaller (which I prefer to use in tandem with --onedir so that Plover can be updated by just updating the egg, instead of having to regenerate the whole distribution every time).

Can you list the content of the generated plover.app/plover.dmg? Is plover bundled as a zip inside it?

Alternatively, you can try PyInstaller support bundling eggs:

  • build Plover egg: python2 setup.py bdist_egg
  • copy launch.py to dist: cp launch.py dist/
  • from inside dist, use PyInstaller like so: env PYTHONPATH=plover-2.5.8-py2.7.egg pyinstaller -w --name plover --onedir --distpath . launch.py

Check in dist/plover/eggs if plover egg is present. Note that I could not get the latest (3.0) to work, I add to install version 2.1 (pip install --user 'pyinstaller==2.1').

@morinted
Copy link
Member

morinted commented Nov 7, 2015

The one created with the commands you listed above is twice the size (probably because of the egg?) and doesn't start -- gives no visual feedback and system console says "abnormal exit code 255". Attached

I used PyInstaller 2.1. I'll try with 3.0 and see if there's a difference.

The make dist/Plover.app output (which complains about the resource_filename), attached

EDIT: updated wrong second link.

@benoit-pierre
Copy link
Member Author

Regarding the size difference:

> ds egged/plover.app/Contents/MacOS/libwx_osx_cocoau-3.0.0.2.0.dylib
40M egged/plover.app/Contents/MacOS/libwx_osx_cocoau-3.0.0.2.0.dylib
> ds make/Plover.app/Contents/Resources/lib/python2.7/lib-dynload/wx
8M make/Plover.app/Contents/Resources/lib/python2.7/lib-dynload/wx

Ouch! But the make version uses this ditto --arch i386 dist/Plover.app dist/PloverStripped.app, I guess to strip libraries?

If you still want to make PyInstaller work, I would try without 74a05b5 first.

Or keep using py2app and just change config.py as follow:

# If plover is run from a pyinstaller binary.
if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
    ASSETS_DIR = sys._MEIPASS
    PROGRAM_DIR = dirname(sys.executable)
# If plover is run from an app bundle on Mac.
elif (sys.platform.startswith('darwin') and '.app' in realpath(__file__)):
    ASSETS_DIR = os.getcwd()
    PROGRAM_DIR = abspath(join(dirname(sys.executable), *[pardir] * 3))
else:
    ASSETS_DIR = join(dirname(dirname(realpath(__file__))), 'assets')
    PROGRAM_DIR = os.getcwd()

if not sys.platform.startswith('darwin') and \
   pkg_resources.resource_isdir('plover', 'assets'):
    ASSETS_DIR = pkg_resources.resource_filename('plover', 'assets')

@balshetzer
Copy link
Member

The stripping is for something very specific:
OSX has binaries that contain multiple binaries in it. In this case the
python binary has both 32bit and 64bit binaries in it. When it starts it
uses a variety of methods to choose which one to run. The version of
wxpython we're using only supports 32bit on OSX. This means we must run the
32bit python interpreter. The easiest way to do that is to just strip out
the 64bit version.
On Nov 7, 2015 4:29 PM, "Benoit Pierre" notifications@github.com wrote:

Regarding the size difference:

ds egged/plover.app/Contents/MacOS/libwx_osx_cocoau-3.0.0.2.0.dylib
40M egged/plover.app/Contents/MacOS/libwx_osx_cocoau-3.0.0.2.0.dylib
ds make/Plover.app/Contents/Resources/lib/python2.7/lib-dynload/wx
8M make/Plover.app/Contents/Resources/lib/python2.7/lib-dynload/wx

Ouch! But the make version uses this ditto --arch i386 dist/Plover.app
dist/PloverStripped.app, I guess to strip libraries?

If you still want to make PyInstaller work, I would try without 74a05b5
74a05b5
first.

Or keep using py2app and just change config.py as follow:

If plover is run from a pyinstaller binary.if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):

ASSETS_DIR = sys._MEIPASS
PROGRAM_DIR = dirname(sys.executable)# If plover is run from an app bundle on Mac.elif (sys.platform.startswith('darwin') and '.app' in realpath(__file__)):
ASSETS_DIR = os.getcwd()
PROGRAM_DIR = abspath(join(dirname(sys.executable), *[pardir] * 3))else:
ASSETS_DIR = join(dirname(dirname(realpath(__file__))), 'assets')
PROGRAM_DIR = os.getcwd()

if not sys.platform.startswith('darwin') and
pkg_resources.resource_isdir('plover', 'assets'):
ASSETS_DIR = pkg_resources.resource_filename('plover', 'assets')


Reply to this email directly or view it on GitHub
#291 (comment)
.

@benoit-pierre
Copy link
Member Author

@balshetzer: OK, thanks for the info.

@morinted: a better patch for config.py:

diff --git i/plover/oslayer/config.py w/plover/oslayer/config.py
index 868639b..a4f6c28 100644
--- i/plover/oslayer/config.py
+++ w/plover/oslayer/config.py
@@ -9,20 +9,23 @@ import os
 from os.path import realpath, join, dirname, abspath, isfile, pardir
 import sys

+bundle_type = None

 # If plover is run from a pyinstaller binary.
 if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
     ASSETS_DIR = sys._MEIPASS
     PROGRAM_DIR = dirname(sys.executable)
+    bundle_type = 'pyinstaller'
 # If plover is run from an app bundle on Mac.
 elif (sys.platform.startswith('darwin') and '.app' in realpath(__file__)):
     ASSETS_DIR = os.getcwd()
     PROGRAM_DIR = abspath(join(dirname(sys.executable), *[pardir] * 3))
+    bundle_type = 'mac'
 else:
     ASSETS_DIR = join(dirname(dirname(realpath(__file__))), 'assets')
     PROGRAM_DIR = os.getcwd()

-if pkg_resources.resource_isdir('plover', 'assets'):
+if bundle_type != 'mac' and pkg_resources.resource_isdir('plover', 'assets'):
     ASSETS_DIR = pkg_resources.resource_filename('plover', 'assets')

 # If the program's directory has a plover.cfg file then run in "portable mode",

This way eggs are still supported when plover is not run from a mac bundle.

@benoit-pierre
Copy link
Member Author

The application class/name is wrong when using ./launch.py. Let me look into it, and I'll push an update with:

  • a fix for this
  • the above patch to config.py
  • the fix for not removing launch.py

OK?

@morinted
Copy link
Member

morinted commented Nov 7, 2015

Yes sir. You are great.
On Nov 7, 2015 5:22 PM, "Benoit Pierre" notifications@github.com wrote:

The application class/name is wrong when using ./launch.py. Let me look
into it, and I'll push an update with:

  • a fix for this
  • the above patch to config.py
  • the fix for not removing launch.py

OK?


Reply to this email directly or view it on GitHub
#291 (comment)
.

@morinted
Copy link
Member

morinted commented Nov 7, 2015

As an aside, PEP8 highlighted unused imports, might want to check that out.

I also would appreciate if you were able to port over the meta data like
version over to OSX's setup.py. Not necessary though, I can do that change
myself afterwards.
On Nov 7, 2015 5:25 PM, "Theodore Morin" morinted@gmail.com wrote:

Yes sir. You are great.
On Nov 7, 2015 5:22 PM, "Benoit Pierre" notifications@github.com wrote:

The application class/name is wrong when using ./launch.py. Let me look
into it, and I'll push an update with:

  • a fix for this
  • the above patch to config.py
  • the fix for not removing launch.py

OK?


Reply to this email directly or view it on GitHub
#291 (comment)
.

@benoit-pierre
Copy link
Member Author

OK, can you see if you can still build the Mac OS X version with those additional changes?

Particularly, are Plover assets correctly included? If not, please try uncommenting line 28 in setup.py. If that still does not work (it should according to py2app documentation), I'll patch it to build the resources list manually.

Note: I'll rewrite this branch to squash the fixups when you're okay with the changes so you can merge a clean history.

@morinted
Copy link
Member

morinted commented Nov 8, 2015

Sure! I'm out for another three hours but I'll still get to it tonight.
On Nov 7, 2015 6:44 PM, "Benoit Pierre" notifications@github.com wrote:

OK, can you see if you can still build the Mac OS X version with those
additional changes?

Particularly, are Plover assets correctly included? If not, please try
uncommenting line 28 in setup.py. If that still does not work (it should
according to py2app documentation), I'll patch it to build the resources
list manually.

Note: I'll rewrite this branch to squash the fixups when you're okay with
the changes so you can merge a clean history.


Reply to this email directly or view it on GitHub
#291 (comment)
.

@morinted
Copy link
Member

morinted commented Nov 8, 2015

You moved dist up to the root, so I had to use make ../dist/Plover.app

pkg_resources.DistributionNotFound: The 'python-xlib>=0.14' distribution was not found and is required by the application

Got rid of that in /setup.py.

error: You must specify either app or plugin

Added app = ['./launch.py'],

Then the resources weren't found. Uncommented that line...

Still not found. Seems they are packed in Contents/Resources/assets. Let's change that setup.py line to 'resources': 'plover/assets/',...

Hey, it seems to work. Cool.

As a note, on OSX it would probably be better to change the name to "Plover" instead of "plover".

diff --git a/setup.py b/setup.py
index a2436a5..2ad064d 100755
--- a/setup.py
+++ b/setup.py
@@ -23,11 +23,12 @@ if sys.platform.startswith('darwin'):
     options['py2app'] = {
         'argv_emulation': True,
         'iconfile': 'osx/plover.icns',
-        # 'resources': 'plover/assets',
+        'resources': 'plover/assets/',
     }

 setuptools.setup(
     name = __software_name__,
+    app = ['./launch.py'],
     version = __version__,
     description = __description__,
     long_description = __long_description__,
@@ -44,7 +45,6 @@ setuptools.setup(
     install_requires = [
         'setuptools',
         'pyserial>=2.7',
-        'python-xlib>=0.14',
         'appdirs>=1.4.0',
         'wxPython>=2.8'
     ],

You will probably want to conditionally add that x11 lib, but otherwise now things are working. Built a .dmg, too.

@benoit-pierre
Copy link
Member Author

OK, made a few more changes:

  • fixed dependencies in setup.py (with platform specific dependencies, tested on Linux and with Wine, hopefully Mac OS X will work too)
  • made the changes you needed for Mac OS X in setup.py
  • I tweaked the osx/makefile: I added phony rules for app and dmg. Shouldn't the dmg file be generated in dist too?

Note: this is disabled when run from a Mac OS X bundle,
since it's unsupported by py2app.
@morinted
Copy link
Member

All right, this is getting good and close. I think "Plover" should be capitalized; shows lowercase in applications and the menu bar.

I'd appreciate the Plover.dmg being built in the dist directory, and then that won't need to be in the .gitignore.

Otherwise I like this, thanks for the effort.

@benoit-pierre
Copy link
Member Author

OK, done:

  • history rewritten to clean it up
  • "plover" -> "Plover"
  • Plover.dmg moved to dist
  • PEP8 cleanup in setup.py

@morinted
Copy link
Member

Looks good, thanks. I think I've hit a rock, though. I was playing with wxPython versions and discovered that wxPython doesn't install on OSX 10.11. I'm trying to work through this, but building isn't working. It's in progress, but for a little I will be MIA for Mac builds.

morinted added a commit that referenced this pull request Nov 12, 2015
A number of improvements to the dev environment
@morinted morinted merged commit e5c4aeb into openstenoproject:master Nov 12, 2015
@morinted
Copy link
Member

Running on Windows today, I think this broke the building of EXEs. See http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal

On launch of the exe, I get an error --> file exists and shouldn't

@morinted
Copy link
Member

To the PyInstaller spec I added: a.datas = list({tuple(map(str.upper, t)) for t in a.datas})

and Now when I run the exe I get

$ ./dist/plover.exe
Traceback (most recent call last):
  File "<string>", line 3, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "C:\Users\morin\git\plover\windows\build\pyinstaller\out00-PYZ.pyz\plover.main", line 22, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "C:\Users\morin\git\plover\windows\build\pyinstaller\out00-PYZ.pyz\plover.gui.main", line 16, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "C:\Users\morin\git\plover\windows\build\pyinstaller\out00-PYZ.pyz\plover.app", line 20, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "C:\Users\morin\git\plover\windows\build\pyinstaller\out00-PYZ.pyz\plover.config", line 13, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "C:\Users\morin\git\plover\windows\build\pyinstaller\out00-PYZ.pyz\plover.oslayer.config", line 28, in <module>
  File "C:\Users\morin\git\plover\windows\build\pyinstaller\out00-PYZ.pyz\pkg_resources", line 1149, in resource_isdir
  File "C:\Users\morin\git\plover\windows\build\pyinstaller\out00-PYZ.pyz\pkg_resources", line 1618, in resource_isdir
  File "C:\Users\morin\git\plover\windows\build\pyinstaller\out00-PYZ.pyz\pkg_resources", line 1658, in _isdir
NotImplementedError: Can't perform this operation for unregistered loader type

@benoit-pierre
Copy link
Member Author

Which version? 3.0 or 2.1? Looking into it too...

@morinted
Copy link
Member

Looks like I'm using 2.1. Tried 3.0 -- no good at all (-1 on launch, only 5MB)

@benoit-pierre
Copy link
Member Author

It's the pkg_resources stuff, don't know why it's not working anymore... Looking for a fix...

@benoit-pierre
Copy link
Member Author

OK, so:

I was able to successfully build Plover.exe with pyinstaller 2.1 and the following script:

#!/usr/bin/env python2

import subprocess
import sys
import os

sys.path.insert(0, '..')

from plover import (
    __name__ as __software_name__,
    __version__,
)

if sys.platform.startswith('win32'):
    python = ['python.exe']
    pyinstaller = ['pyinstaller.exe']
else:
    python = ['python2']
    pyinstaller = ['wine', 'pyinstaller.exe']

name = __software_name__.capitalize()
egg = '../dist/%s-%s-py%u.%u.egg' % ((name, __version__) +
                                     sys.version_info[0:2])

print 'creating egg'
subprocess.check_call(python + ['setup.py', '--quiet', 'bdist_egg'], cwd='..')
assert os.path.exists(egg)
print 'running pyinstaller'
subprocess.check_call(pyinstaller + ['--workpath=../build',
                                     '--specpath=../build',
                                     '--distpath=../dist',
                                     '--log-level=WARN',
                                     '--onefile',
                                     '--paths=%s' % egg,
                                     '--name=%s' % name,
                                     '--icon=%s.ico' % name,
                                     '../build/launch.py'])

(saved as windows/build.py)

Can you check if this work for you? If it does I can make a pull request to add it and get rid of build.bat and pyinstaller.spec (which does contain some obsolete options, e.g. upx).

@morinted
Copy link
Member

I had to change your script to be ../launch.py instead of ../build/launch.py. However, the output file, when launched, opens as a command prompt, and then outputs the same error:

Traceback (most recent call last):
  File "<string>", line 3, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "../build\Plover\out00-PYZ.pyz\plover.main", line 22, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "../build\Plover\out00-PYZ.pyz\plover.gui.main", line 16, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "../build\Plover\out00-PYZ.pyz\plover.app", line 20, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "../build\Plover\out00-PYZ.pyz\plover.config", line 13, in <module>
  File "C:\Python27\Lib\site-packages\PyInstaller\loader\pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "../build\Plover\out00-PYZ.pyz\plover.oslayer.config", line 28, in <module>
  File "../build\Plover\out00-PYZ.pyz\pkg_resources", line 1149, in resource_isdir
  File "../build\Plover\out00-PYZ.pyz\pkg_resources", line 1618, in resource_isdir
  File "../build\Plover\out00-PYZ.pyz\pkg_resources", line 1658, in _isdir
NotImplementedError: Can't perform this operation for unregistered loader type
WARNING: file already exists but should not: C:\Users\morin\AppData\Local\Temp\_MEI91162\include\pyconfig.h

@benoit-pierre
Copy link
Member Author

It's normal, I forgot to copy 'launch.py' to 'build/launch.py': this is required, otherwise pyinstaller will automatically add the top directory (containing the original 'launch.py') to the path, and pick up plover from the 'plover' directory instead of the egg.

@benoit-pierre
Copy link
Member Author

Can you copy it manually and test? Meanwhile I'll make a few adjustments and create a pull request.

@morinted
Copy link
Member

So I get that warning

$ ../dist/Plover.exe
WARNING: file already exists but should not: C:\Users\morin\AppData\Local\Temp\_MEI55682\include\pyconfig.h

Then Plover starts up as it should. Will you be able to get rid of the open command window that's created when I double click the .exe in Explorer?

@benoit-pierre
Copy link
Member Author

Adding '--windowed' should take care of that.

@morinted
Copy link
Member

Good good... I've got to head out now but I'll be back, likely tomorrow
morning. You can submit the Mac PR if you are okay with it.
On Nov 14, 2015 6:46 PM, "Benoit Pierre" notifications@github.com wrote:

Adding '--windowed' should take care of that.


Reply to this email directly or view it on GitHub
#291 (comment)
.

@benoit-pierre
Copy link
Member Author

So it works without '--onefile' (with pyinstaller 2.1, branch here], but with it we're hitting another pyinstaller bug (pyinstaller/pyinstaller#782) which results in a annoying warning popup on startup...

So let's be pragmatic and go back to not bundling an egg when using pyinstaller: #303

@benoit-pierre benoit-pierre deleted the better-dev-env branch February 18, 2016 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants