Golang Build: Initial Sublime Text build package implementation
The documentation in the docs/ dir includes information about how
to use the package and perform any necessary configuration.
Change-Id: I132fdd2300f850d07724d76feb50361abc9e5e02
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b638fce
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.tmLanguage.cache
+*.pyc
+__pycache__
+dev/coverage_reports/
+dev/go_projects/src/github.com/
+dev/go_projects/pkg/
+dev/go_projects/bin/
diff --git a/.pep8 b/.pep8
new file mode 100644
index 0000000..6deafc2
--- /dev/null
+++ b/.pep8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 120
diff --git a/Default.sublime-commands b/Default.sublime-commands
new file mode 100644
index 0000000..93b893b
--- /dev/null
+++ b/Default.sublime-commands
@@ -0,0 +1,18 @@
+[
+ {
+ "caption": "Golang Build: Get",
+ "command": "golang_build_get"
+ },
+ {
+ "caption": "Golang Build: Cancel",
+ "command": "golang_build_cancel"
+ },
+ {
+ "caption": "Golang Build: Reopen Output",
+ "command": "golang_build_reopen"
+ },
+ {
+ "caption": "Golang Build: Terminal",
+ "command": "golang_build_terminal"
+ }
+]
\ No newline at end of file
diff --git a/Golang Build Output.tmLanguage b/Golang Build Output.tmLanguage
new file mode 100644
index 0000000..f09909c
--- /dev/null
+++ b/Golang Build Output.tmLanguage
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>name</key>
+ <string>Golang Build Output</string>
+ <key>patterns</key>
+ <array>
+ <dict>
+ <key>captures</key>
+ <dict>
+ <key>1</key>
+ <dict>
+ <key>name</key>
+ <string>comment.line.double-slash.go</string>
+ </dict>
+ <key>2</key>
+ <dict>
+ <key>name</key>
+ <string>markup.inserted.diff</string>
+ </dict>
+ </dict>
+ <key>match</key>
+ <string>^(> Result: )(Success)$</string>
+ </dict>
+ <dict>
+ <key>captures</key>
+ <dict>
+ <key>1</key>
+ <dict>
+ <key>name</key>
+ <string>comment.line.double-slash.go</string>
+ </dict>
+ <key>2</key>
+ <dict>
+ <key>name</key>
+ <string>markup.deleted.diff</string>
+ </dict>
+ </dict>
+ <key>match</key>
+ <string>^(> Result: )(Error|Cancelled)$</string>
+ </dict>
+ <dict>
+ <key>captures</key>
+ <dict>
+ <key>1</key>
+ <dict>
+ <key>name</key>
+ <string>comment.line.double-slash.go</string>
+ </dict>
+ <key>2</key>
+ <dict>
+ <key>name</key>
+ <string>markup.changed.diff</string>
+ </dict>
+ </dict>
+ <key>match</key>
+ <string>^(> Elapsed: )(.*)$</string>
+ </dict>
+ <dict>
+ <key>match</key>
+ <string>^(> (Directory|Environment|Command|Output):[ \n])(.*)$</string>
+ <key>name</key>
+ <string>comment.line.double-slash.go</string>
+ </dict>
+ <dict>
+ <key>match</key>
+ <string>^(> )(\w+)(=)(.*)$</string>
+ <key>name</key>
+ <string>comment.line.double-slash.go</string>
+ </dict>
+ </array>
+ <key>scopeName</key>
+ <string>output.golang_build</string>
+ <key>uuid</key>
+ <string>E3A415F0-3F50-11E0-9207-1911300C9A67</string>
+</dict>
+</plist>
diff --git a/Golang Build.sublime-build b/Golang Build.sublime-build
new file mode 100644
index 0000000..006cbf1
--- /dev/null
+++ b/Golang Build.sublime-build
@@ -0,0 +1,23 @@
+{
+ "target": "golang_build",
+ "selector": "source.go",
+
+ "variants": [
+ {
+ "name": "Test",
+ "task": "test"
+ },
+ {
+ "name": "Install",
+ "task": "install"
+ },
+ {
+ "name": "Cross-Compile (Interactive)",
+ "task": "cross_compile"
+ },
+ {
+ "name": "Clean",
+ "task": "clean"
+ }
+ ]
+}
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..3e26c6e
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,5 @@
+# *Golang Build* Changelog
+
+## 1.0.0
+
+ - Initial release
diff --git a/dependencies.json b/dependencies.json
new file mode 100644
index 0000000..5860b5a
--- /dev/null
+++ b/dependencies.json
@@ -0,0 +1,10 @@
+{
+ "*": {
+ "*": [
+ "shellenv",
+ "golangconfig",
+ "newterm",
+ "package_events"
+ ]
+ }
+}
diff --git a/dev/__init__.py b/dev/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dev/__init__.py
diff --git a/dev/go_projects/src/bad/hello.go b/dev/go_projects/src/bad/hello.go
new file mode 100644
index 0000000..607814c
--- /dev/null
+++ b/dev/go_projects/src/bad/hello.go
@@ -0,0 +1,5 @@
+package main
+
+func main() {
+ fmt.Printf("Hello, world.\n")
+}
diff --git a/dev/go_projects/src/good/rune_len.go b/dev/go_projects/src/good/rune_len.go
new file mode 100644
index 0000000..765d46c
--- /dev/null
+++ b/dev/go_projects/src/good/rune_len.go
@@ -0,0 +1,9 @@
+package good
+
+func RuneLen(s string) int {
+ i := 0
+ for range s {
+ i += 1
+ }
+ return i
+}
diff --git a/dev/go_projects/src/good/rune_len_test.go b/dev/go_projects/src/good/rune_len_test.go
new file mode 100644
index 0000000..2062458
--- /dev/null
+++ b/dev/go_projects/src/good/rune_len_test.go
@@ -0,0 +1,24 @@
+package good
+
+import (
+ "testing"
+)
+
+func TestRuneLen(t *testing.T) {
+ cases := []struct {
+ s string
+ byteLength int
+ runeLength int
+ }{
+ {"résumé", 8, 6},
+ {"résumé – new", 16, 12},
+ }
+ for _, c := range cases {
+ if len(c.s) != c.byteLength {
+ t.Errorf("len(%q) != %d", c.s, c.byteLength)
+ }
+ if RuneLen(c.s) != c.runeLength {
+ t.Errorf("RuneLen(%q) != %d", c.s, c.runeLength)
+ }
+ }
+}
diff --git a/dev/mocks.py b/dev/mocks.py
new file mode 100644
index 0000000..b471e3e
--- /dev/null
+++ b/dev/mocks.py
@@ -0,0 +1,112 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import os
+import sys
+import locale
+
+import sublime
+import golangconfig
+
+if sys.version_info < (3,):
+ golang_build = sys.modules['golang_build']
+else:
+ golang_build = sys.modules['Golang Build.golang_build']
+
+
+class ShellenvMock():
+
+ _env_encoding = locale.getpreferredencoding() if sys.platform == 'win32' else 'utf-8'
+ _fs_encoding = 'mbcs' if sys.platform == 'win32' else 'utf-8'
+
+ _shell = None
+ _data = None
+
+ def __init__(self, shell, data):
+ self._shell = shell
+ self._data = data
+
+ def get_env(self, for_subprocess=False):
+ if not for_subprocess or sys.version_info >= (3,):
+ return (self._shell, self._data)
+
+ shell = self._shell.encode(self._fs_encoding)
+ env = {}
+ for name, value in self._data.items():
+ env[name.encode(self._env_encoding)] = value.encode(self._env_encoding)
+
+ return (shell, env)
+
+ def get_path(self):
+ return (self._shell, self._data.get('PATH', '').split(os.pathsep))
+
+ def env_encode(self, value):
+ if sys.version_info >= (3,):
+ return value
+ return value.encode(self._env_encoding)
+
+ def path_encode(self, value):
+ if sys.version_info >= (3,):
+ return value
+ return value.encode(self._fs_encoding)
+
+ def path_decode(self, value):
+ if sys.version_info >= (3,):
+ return value
+ return value.decode(self._fs_encoding)
+
+
+class SublimeSettingsMock():
+
+ _values = None
+
+ def __init__(self, values):
+ self._values = values
+
+ def get(self, name, default=None):
+ return self._values.get(name, default)
+
+
+class SublimeMock():
+
+ _settings = None
+ View = sublime.View
+ Window = sublime.Window
+
+ def __init__(self, settings):
+ self._settings = SublimeSettingsMock(settings)
+
+ def load_settings(self, basename):
+ return self._settings
+
+
+class GolangBuildMock():
+
+ _shellenv = None
+ _sublime = None
+
+ _shell = None
+ _env = None
+ _sublime_settings = None
+
+ def __init__(self, shell=None, env=None, sublime_settings=None):
+ self._shell = shell
+ self._env = env
+ self._sublime_settings = sublime_settings
+
+ def __enter__(self):
+ if self._shell is not None and self._env is not None:
+ self._shellenv = golangconfig.shellenv
+ golangconfig.shellenv = ShellenvMock(self._shell, self._env)
+ golang_build.shellenv = golangconfig.shellenv
+ if self._sublime_settings is not None:
+ self._sublime = golangconfig.sublime
+ golangconfig.sublime = SublimeMock(self._sublime_settings)
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self._shellenv is not None:
+ golangconfig.shellenv = self._shellenv
+ golang_build.shellenv = self._shellenv
+ if self._sublime is not None:
+ golangconfig.sublime = self._sublime
diff --git a/dev/reloader.py b/dev/reloader.py
new file mode 100644
index 0000000..8ceb6e7
--- /dev/null
+++ b/dev/reloader.py
@@ -0,0 +1,23 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import sys
+from os import path
+import time
+
+
+module_name = 'golang_build'
+if sys.version_info >= (3,):
+ module_name = 'Golang Build.' + module_name
+ from imp import reload
+
+if module_name in sys.modules:
+ reload(sys.modules[module_name])
+
+if 'Golang Build.dev.mocks' in sys.modules:
+ reload(sys.modules['Golang Build.dev.mocks'])
+
+filepath = path.join(path.dirname(__file__), '..', 'golang_build.py')
+open(filepath, 'ab').close()
+# Wait for Sublime Text to reload the file
+time.sleep(0.5)
diff --git a/dev/tests.py b/dev/tests.py
new file mode 100644
index 0000000..51c17b4
--- /dev/null
+++ b/dev/tests.py
@@ -0,0 +1,507 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import sys
+import threading
+import unittest
+from os import path
+import time
+import re
+import shutil
+import os
+
+import sublime
+
+import shellenv
+import package_events
+
+if sys.version_info < (3,):
+ from Queue import Queue
+else:
+ from queue import Queue
+
+from .mocks import GolangBuildMock
+
+
+TEST_GOPATH = path.join(path.dirname(__file__), 'go_projects')
+VIEW_SETTINGS = {
+ 'GOPATH': TEST_GOPATH,
+ 'GOOS': None,
+ 'GOARCH': None,
+ 'GOARM': None,
+ 'GO386': None,
+ 'GORACE': None
+}
+
+CROSS_COMPILE_OS = 'darwin' if sys.platform != 'darwin' else 'linux'
+
+
+class GolangBuildTests(unittest.TestCase):
+
+ def setUp(self):
+ for subdir in ('pkg', 'bin', 'src'):
+ full_path = path.join(TEST_GOPATH, subdir)
+ for entry in os.listdir(full_path):
+ if entry in set(['.git-keep', 'good', 'bad']):
+ continue
+ entry_path = path.join(full_path, entry)
+ if path.isdir(entry_path):
+ shutil.rmtree(entry_path)
+ else:
+ os.remove(entry_path)
+
+ def test_build(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build')
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go build" succeed?'))
+
+ def test_build_flags(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build', {'flags': ['-x']})
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go build" succeed and print all commands?'))
+
+ def test_build_flags_from_settings(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ with GolangBuildMock(sublime_settings={'build:flags': ['-x']}):
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build')
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go build" succeed and print all commands?'))
+
+ def test_install_flags_from_view_settings(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build', {'task': 'install'})
+
+ custom_view_settings = VIEW_SETTINGS.copy()
+ custom_view_settings['install:flags'] = ['-x']
+
+ result_queue = open_file(file_path, custom_view_settings, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go install" succeed and print all commands?'))
+
+ def test_clean(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build', {'task': 'clean'})
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go clean" succeed?'))
+
+ def test_test(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build', {'task': 'test'})
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go test" succeed?'))
+
+ def test_install(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build', {'task': 'install'})
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go install" succeed?'))
+
+ def test_cross_compile(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+ begin_event = threading.Event()
+
+ def _run_build(view, result_queue):
+ notify_user('Select %s/amd64 from quick panel' % CROSS_COMPILE_OS)
+ begin_event.set()
+ view.window().run_command('golang_build', {'task': 'cross_compile'})
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ begin_event.wait()
+ result = wait_build(result_queue, timeout=15)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did the cross-compile succeed?'))
+
+ def test_get(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+ begin_event = threading.Event()
+
+ def _run_build(view, result_queue):
+ sublime.set_clipboard('github.com/golang/example/hello')
+ notify_user('Paste from the clipboard into the input panel')
+ begin_event.set()
+ view.window().run_command('golang_build_get')
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ begin_event.wait()
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go get" succeed?'))
+
+ def test_get_flags(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+ begin_event = threading.Event()
+
+ def _run_build(view, result_queue):
+ sublime.set_clipboard('github.com/golang/example/hello')
+ notify_user('Paste from the clipboard into the input panel')
+ begin_event.set()
+ view.window().run_command('golang_build_get', {'flags': ['-d']})
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ begin_event.wait()
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go get" download but not install?'))
+
+ def test_get_flags_from_settings(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ with GolangBuildMock(sublime_settings={'get:flags': ['-d']}):
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build_get', {'url': 'github.com/golang/example/hello'})
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go get" download but not install?'))
+
+ def test_get_url(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build_get', {'url': 'github.com/golang/example/hello'})
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+ self.assertTrue(confirm_user('Did "go get" succeed for "github.com/golang/example/hello"?'))
+
+ def test_terminal(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build_terminal')
+
+ open_file(file_path, VIEW_SETTINGS, _run_build)
+ self.assertTrue(confirm_user('Did a terminal open to Packages/Golang Build/dev/go_projects/src/good/?'))
+
+ def test_build_bad(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'bad', 'hello.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build')
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('error', result)
+ self.assertTrue(confirm_user('Did "go build" fail?'))
+
+ def test_build_cancel(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build')
+
+ def _cancel_build():
+ view.window().run_command('golang_build_cancel')
+
+ sublime.set_timeout(_cancel_build, 50)
+
+ # We perform a cross-compile so the user has time to interrupt the build
+ custom_view_settings = VIEW_SETTINGS.copy()
+ custom_view_settings['GOOS'] = CROSS_COMPILE_OS
+ custom_view_settings['GOARCH'] = 'amd64'
+
+ result_queue = open_file(file_path, custom_view_settings, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('cancelled', result)
+ self.assertTrue(confirm_user('Was "go build" successfully cancelled?'))
+
+ def test_build_reopen(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ view.window().run_command('golang_build')
+
+ result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+ result = wait_build(result_queue)
+ self.assertEqual('success', result)
+
+ time.sleep(0.4)
+
+ def _hide_panel():
+ sublime.active_window().run_command('hide_panel')
+ sublime.set_timeout(_hide_panel, 1)
+
+ time.sleep(0.4)
+ self.assertTrue(confirm_user('Was the build output hidden?'))
+
+ def _reopen_panel():
+ sublime.active_window().run_command('golang_build_reopen')
+ sublime.set_timeout(_reopen_panel, 1)
+
+ time.sleep(0.4)
+ self.assertTrue(confirm_user('Was the build output reopened?'))
+
+ def test_build_interrupt(self):
+ ensure_not_ui_thread()
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+ begin_event = threading.Event()
+ second_begin_event = threading.Event()
+
+ def _run_build(view, result_queue):
+ notify_user('Press the "Stop Running Build" button when prompted')
+
+ begin_event.set()
+ view.window().run_command('golang_build')
+
+ def _new_build():
+ view.window().run_command('golang_build')
+ second_begin_event.set()
+
+ sublime.set_timeout(_new_build, 50)
+
+ # We perform a cross-compile so the user has time to interrupt the build
+ custom_view_settings = VIEW_SETTINGS.copy()
+ custom_view_settings['GOOS'] = CROSS_COMPILE_OS
+ custom_view_settings['GOARCH'] = 'amd64'
+
+ result_queue = open_file(file_path, custom_view_settings, _run_build)
+ begin_event.wait()
+ result1 = wait_build(result_queue)
+ self.assertEqual('cancelled', result1)
+ second_begin_event.wait()
+ result2 = wait_build(result_queue)
+ self.assertEqual('success', result2)
+ self.assertTrue(confirm_user('Was the first build cancelled and the second successful?'))
+
+ def test_build_go_missing(self):
+ ensure_not_ui_thread()
+
+ shell, _ = shellenv.get_env()
+ search_path = path.expanduser('~')
+ with GolangBuildMock(shell=shell, env={'PATH': search_path}):
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ notify_user('Press the "Open Documentation" button when prompted about go not being found in the PATH')
+
+ view.window().run_command('golang_build')
+
+ open_file(file_path, VIEW_SETTINGS, _run_build)
+ time.sleep(0.5)
+ self.assertTrue(confirm_user('Were you prompted that go could not be found in the PATH?'))
+ self.assertTrue(confirm_user('When you pressed "Open Documentation", was it opened in your browser?'))
+
+ def test_build_no_gopath(self):
+ ensure_not_ui_thread()
+
+ shell, env = shellenv.get_env()
+ if 'GOPATH' in env:
+ del env['GOPATH']
+ with GolangBuildMock(shell=shell, env=env):
+
+ file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+ def _run_build(view, result_queue):
+ notify_user('Press the "Open Documentation" button when prompted about GOPATH not being set')
+
+ view.window().run_command('golang_build')
+
+ custom_view_settings = VIEW_SETTINGS.copy()
+ del custom_view_settings['GOPATH']
+ open_file(file_path, custom_view_settings, _run_build)
+ time.sleep(0.5)
+ self.assertTrue(confirm_user('Were you prompted that GOPATH was not set?'))
+ self.assertTrue(confirm_user('When you pressed "Open Documentation", was it opened in your browser?'))
+
+
+def ensure_not_ui_thread():
+ """
+ The tests won't function properly if they are run in the UI thread, so
+ this functions throws an exception if that is attempted
+ """
+
+ if isinstance(threading.current_thread(), threading._MainThread):
+ raise RuntimeError('Tests can not be run in the UI thread')
+
+
+def open_file(file_path, view_settings, callback):
+ """
+ Open a file in Sublime Text, sets settings on the view and then executes
+ the callback once the file is opened
+
+ :param file_path:
+ A unicode string of the path to the file to open
+
+ :param view_settings:
+ A dict of settings to set the "golang" key of the view's settings to
+
+ :param callback:
+ The callback to execute in the UI thread once the file is opened
+ """
+
+ result_queue = Queue()
+ file_param = file_path
+ if sys.platform == 'win32':
+ file_param = re.sub('^([a-zA-Z]):', '/\\1', file_param)
+ file_param = file_param.replace('\\', '/')
+
+ def open_file_callback():
+ window = sublime.active_window()
+
+ window.run_command(
+ 'open_file',
+ {
+ 'file': file_param
+ }
+ )
+
+ when_file_opened(window, file_path, view_settings, callback, result_queue)
+ sublime.set_timeout(open_file_callback, 50)
+ return result_queue
+
+
+def when_file_opened(window, file_path, view_settings, callback, result_queue):
+ """
+ Periodic polling callback used by open_file() to find the newly-opened file
+
+ :param window:
+ The sublime.Window to look for the view in
+
+ :param file_path:
+ The file path of the file that was opened
+
+ :param view_settings:
+ A dict of settings to set to the view's "golang" setting key
+
+ :param callback:
+ The callback to execute when the file is opened
+
+ :param result_queue:
+ A Queue() object the callback can use to communicate with the test
+ """
+
+ view = window.active_view()
+ if view and view.file_name() == file_path:
+ view.settings().set('golang', view_settings)
+ callback(view, result_queue)
+ return
+ # If the view was not ready, retry a short while later
+ sublime.set_timeout(lambda: when_file_opened(window, file_path, view_settings, callback, result_queue), 50)
+
+
+def wait_build(result_queue, timeout=5):
+ """
+ Uses the result queue to wait for a result from the open_file() callback
+
+ :param result_queue:
+ The Queue() to get the result from
+
+ :param timeout:
+ How long to wait before considering the test a failure
+
+ :return:
+ The value from the queue
+ """
+
+ def _send_result(package_name, event_name, payload):
+ result_queue.put(payload.result)
+
+ try:
+ package_events.listen('Golang Build', _send_result)
+ return result_queue.get(timeout=timeout)
+ finally:
+ package_events.unlisten('Golang Build', _send_result)
+
+
+def confirm_user(message):
+ """
+ Prompts the user to via a dialog to confirm a question
+
+ :param message:
+ A unicode string of the message to present to the user
+
+ :return:
+ A boolean - if the user clicked "Yes"
+ """
+
+ queue = Queue()
+
+ def _show_ok_cancel():
+ response = sublime.ok_cancel_dialog('Test Suite for Golang Build\n\n' + message, 'Yes')
+ queue.put(response)
+
+ sublime.set_timeout(_show_ok_cancel, 1)
+ return queue.get()
+
+
+def notify_user(message):
+ """
+ Open a dialog for the user to inform them of a user interaction that is
+ part of the test suite
+
+ :param message:
+ A unicode string of the message to present to the user
+ """
+
+ sublime.ok_cancel_dialog('Test Suite for Golang Build\n\n' + message, 'Ok')
diff --git a/docs/commands.md b/docs/commands.md
new file mode 100644
index 0000000..457567c
--- /dev/null
+++ b/docs/commands.md
@@ -0,0 +1,80 @@
+# *Golang Build* Commands
+
+This page lists the commands provided by this package and the arguments they
+accept.
+
+ - [Commands](#commands)
+ - [golang_build](#golang_build)
+ - [golang_build_get](#golang_build_get)
+ - [golang_build_terminal](#golang_build_terminal)
+ - [Key Binding Example](#key-binding-example)
+ - [Command Palette Example](#command-palette-example)
+
+## Commands
+
+### golang_build
+
+The `golang_build` command executes various `go` commands and accepts the
+following args:
+
+ - `task`: A string of the build task to perform. Accepts the following values:
+ - `"build"`: executes `go build -v`
+ - `"test"`: executes `go test -v`
+ - `"install"`: executes `go install -v`
+ - `"clean"`: executes `go clean -v`
+ - `"cross_compile"`: executes `go build -v` with `GOOS` and `GOARCH` set
+ - `flags`: A list of strings to pass to the `go` executable as flags. The list
+ of valid flags can be determined by executing `go help {task}` in the
+ terminal.
+
+### golang_build_get
+
+The `golang_build_get` command executes `go get -v` and accepts the following
+args:
+
+ - `url`: A string of the URL to get, instead of prompting the user for it.
+ - `flags`: A list of strings to pass to the `go` executable as flags. The list
+ of valid flags can be determined by executing `go help get` in the
+ terminal.
+
+### golang_build_terminal
+
+The `golang_build_terminal` command opens a terminal to the directory containing
+the currently open file. The command does not accept any args.
+
+## Key Binding Example
+
+The following JSON structure can be added to the file opened by the
+*Preferences > Key Bindings – User* menu entry.
+
+```json
+[
+ {
+ "keys": ["super+ctrl+g", "super+ctrl+t"],
+ "command": "golang_build",
+ "args": {
+ "task": "test",
+ "flags": ["-x"]
+ }
+ }
+]
+```
+
+## Command Palette Example
+
+The following JSON structure can be added to
+`Packages/User/Default.sublime-commands`. The `Packages/` folder can be located
+by the *Preferences > Browse Packages...* menu entry.
+
+```json
+[
+ {
+ "caption": "Golang Build: Test (Print Commands)",
+ "command": "golang_build",
+ "args": {
+ "task": "test",
+ "flags": ["-x"]
+ }
+ }
+]
+```
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000..20ed406
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,198 @@
+# *Golang Build* Configuration
+
+By default, *Golang Build* looks at the user‘s shell environment to find the
+values for various Go environment variables, including `GOPATH`.
+
+In some situations the automatic detection may not be able to properly read the
+desired configuration. In other situations, it may be desirable to provide
+different configuration for different Sublime Text projects.
+
+ - [Environment Autodetection](#environment-autodetection)
+ - [Settings Load Order](#settings-load-order)
+ - [Global Sublime Text Settings](#global-sublime-text-settings)
+ - [OS-Specific Settings](#os-specific-settings)
+ - [Project-Specific Settings](#project-specific-settings)
+ - [Command Flags](#command-flags)
+
+## Environment Autodetection
+
+By default *Golang Build* tries to detect all of your Go configuration by
+invoking your login shell. It will pull in your `PATH`, `GOPATH`, and any other
+environment variables you have set.
+
+## Settings Load Order
+
+Generally, autodetecting the shell environment is sufficient for most users
+with a standard Go environment. If your Go configuration is more complex, or
+you wish to customize command flags, Golang Build will read settings from the
+following sources:
+
+ - [Global Sublime Text Settings](#global-sublime-text-settings)
+ - [OS-Specific Settings](#os-specific-settings)
+ - [Project-Specific Settings](#project-specific-settings)
+
+Settings are loaded using the following precedence, from most-to-least
+specific:
+
+ - OS-specific project settings
+ - OS-specific global Sublime Text settings
+ - Project settings
+ - Global Sublime Text settings
+ - Shell environment
+
+### Global Sublime Text Settings
+
+To set variables for use in Sublime Text windows, you will want to edit your
+`golang.sublime-settings` file. This can be accessed via the menu:
+
+ 1. Preferences
+ 2. Package Settings
+ 3. Golang Config
+ 3. Settings - User
+
+Settings are placed in a json structure. Common settings include:
+
+ - `PATH` - a list of directories to search for executables within. On Windows
+ these are separated by `;`. OS X and Linux use `:` as a directory separator.
+ - `GOPATH` - a string of the path to the root of your Go environment
+
+Other Go environment variables will be used if set. Examples include: `GOOS`,
+`GOARCH`, `GOROOT` and `GORACE`. The
+[go command documentation](https://golang.org/cmd/go/#hdr-Environment_variables)
+has a complete list.
+
+```json
+{
+ "PATH": "/Users/jsmith/go/bin",
+ "GOPATH": "/Users/jsmith/go"
+}
+```
+
+### OS-Specific Settings
+
+For users that are working on different operating systems, it may be necessary
+to segement settings per OS. All settings may be nested under a key of one of
+the following strings:
+
+ - "osx"
+ - "windows"
+ - "linux"
+
+```json
+{
+ "osx": {
+ "PATH": "/Users/jsmith/go/bin",
+ "GOPATH": "/Users/jsmith/go"
+ },
+ "windows": {
+ "PATH": "C:\\Users\\jsmith\\go\\bin",
+ "GOPATH": "C:\\Users\\jsmith\\go"
+ },
+ "linux": {
+ "PATH": "/home/jsmith/go/bin",
+ "GOPATH": "/home/jsmith/go"
+ },
+}
+```
+
+### Project-Specific Settings
+
+When working on Go projects that use different environments, it may be
+necessary to define settings in a
+[Sublime Text project](http://docs.sublimetext.info/en/latest/file_management/file_management.html#projects)
+file. The *Project* menu in Sublime Text provides the interface to create and
+edit project files.
+
+Within projects, all Go settings are placed under the `"settings"` key and then
+further under a subkey named `"golang"`.
+
+```json
+{
+ "folders": {
+ "/Users/jsmith/projects/myproj"
+ },
+ "settings": {
+ "golang": {
+ "PATH": "/Users/jsmith/projects/myproj/env/bin",
+ "GOPATH": "/Users/jsmith/projects/myproj/env"
+ }
+ }
+}
+```
+
+Project-specific settings may also utilize the OS-specific settings feature.
+
+```json
+{
+ "folders": {
+ "/Users/jsmith/projects/myproj"
+ },
+ "settings": {
+ "golang": {
+ "osx": {
+ "PATH": "/Users/jsmith/projects/myproj/env/bin",
+ "GOPATH": "/Users/jsmith/projects/myproj/env"
+ },
+ "linux": {
+ "PATH": "/home/jsmith/projects/myproj/env/bin",
+ "GOPATH": "/home/jsmith/projects/myproj/env"
+ }
+ }
+ }
+}
+```
+
+## Command Flags
+
+When working with the build system, it may be necessary to set flags to pass
+to the `go` executable. Utilizing the various settings locations discussed in
+the [Settings Load Order section](#settings-load-order), each build variant may
+have its flags customized. The settings names are:
+
+ - `build:flags` for "go build"
+ - `test:flags` for "go test"
+ - `install:flags` for "go install"
+ - `clean:flags` for "go clean"
+ - `cross_compile:flags` for "go build" with GOOS and GOARCH
+ - `get:flags` for "go get"
+
+Each setting must have a value that is a list of strings.
+
+The most common location to set these settings will be in a project file:
+
+```json
+{
+ "folders": {
+ "/Users/jsmith/projects/myproj"
+ },
+ "settings": {
+ "golang": {
+ "build:flags": ["-a"],
+ "install:flags": ["-a"],
+ "get:flags": ["-u"]
+ }
+ }
+}
+```
+
+As with the `GOPATH` and `PATH` settings, these flag settings may be set on a
+per-OS basis, even within project files.
+
+An example of settings for just OS X, within a project file:
+
+```json
+{
+ "folders": {
+ "/Users/jsmith/projects/myproj"
+ },
+ "settings": {
+ "golang": {
+ "osx": {
+ "build:flags": ["-a", "-race"],
+ "install:flags": ["-a", "-race"],
+ "get:flags": ["-u"]
+ }
+ }
+ }
+}
+```
diff --git a/docs/design.md b/docs/design.md
new file mode 100644
index 0000000..c20f484
--- /dev/null
+++ b/docs/design.md
@@ -0,0 +1,48 @@
+# *Golang Build* Design
+
+The Golang Build Sublime Text package is structured as follows:
+
+ - The primary user interaction happens through the Sublime Text build system,
+ which parses the "Golang Build.sublime-build" file
+ - The primary build task is "go build", but variants exists for "go test",
+ "go install", "go clean" and cross-compile, which is "go build" with GOOS
+ and GOARCH set. All of these tasks are executing by the Sublime Text command
+ named "golang_build".
+ - Additional Sublime Text commands are implemented that implement the following
+ functionality:
+ - "golang_build_get" provides an interface to "go get"
+ - "golang_terminal" opens a terminal to the Go workspace with all
+ appropriate environment variables set
+ - "golang_build_cancel" allows users to kill an in-process build
+ - "golang_build_reopen" allows users to reopen the build output panel
+ Each of these commands is exposed to the command palette via the file
+ Default.sublime-commands
+ - Configuration uses the Package Control dependency golangconfig, which allows
+ users to set settings globally in Sublime Text, for each OS globally,
+ per-project, or for each OS in a project
+ - Settings exists that allow users to customize command line flags on a
+ per-task-basis
+
+As is dictated by the Sublime Text API, the following list shows a mapping of
+command to Python class name:
+
+ - `golang_build`: `GolangBuildCommand()`
+ - `golang_build_get`: `GolangBuildGetCommand()`
+ - `golang_build_cancel`: `GolangBuildCancelCommand()`
+ - `golang_build_reopen`: `GolangBuildReopenCommand()`
+ - `golang_build_terminal`: `GolangBuildTerminalCommand()`
+
+For `golang_build` and `golang_build_get`, the commands display output to the
+user via an output panel. Normally with Sublime Text when a reference to the
+`sublime.View` object for an output panel is requested, any existing content is
+erased. To prevent a jarring experience for users when a build is interrupted,
+a reference to each window's Golang Build output panel is held in memory and
+re-used when a user interrupts a running build with a new invocation.
+
+The `GolangProcess()` class reprents an invocation of the `go` executable, and
+provides a queue of output information. This output queue is processed by a
+`GolangProcessPrinter()` object which adds environment information before the
+output starts, and summary information once completed. There is one
+`GolangPanel()` object per Sublime Text window, and it contains a lock to ensure
+that only one `GolangProcessPrinter()` may be displaying output at a time to
+prevent interleaved output.
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 0000000..7234327
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,29 @@
+# *Golang Build* Development
+
+## Setup
+
+ - Install [Package Coverage](https://packagecontrol.io/packages/Package%20Coverage)
+ to run tests
+ - Install this package by executing
+ `git clone https://go.googlesource.com/sublime-build "Golang Build"`
+ inside of your `Packages/` folder
+ - Use the Package Control command "Satisfy Dependencies" to install the
+ `shellenv`, `newterm`, `package_events` and `golangconfig` dependencies
+ and then restart Sublime Text
+
+## General Notes
+
+ - All code must pass the checks of the Sublime Text package
+ [Python Flake8 Lint](https://packagecontrol.io/packages/Python%20Flake8%20Lint).
+ The `python_interpreter` setting should be set to `internal`.
+ - Tests and coverage measurement can not be run in the UI thread since the
+ tests interact with the user interface and would become deadlocked
+ - Sublime Text 2 and 3 must be supported, on Windows, OS X and Linux
+ - All functions must include a full docstring with parameter and return types
+ and a list of exceptions raised
+ - All code should use a consistent Python header
+
+```python
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+```
diff --git a/docs/readme.md b/docs/readme.md
new file mode 100644
index 0000000..dfb1ffe
--- /dev/null
+++ b/docs/readme.md
@@ -0,0 +1,35 @@
+# Golang Build
+
+*Golang Build* is a Sublime Text package for compiling Go projects. It provides
+integration between Sublime Text and the command line `go` tool.
+
+The package consists of the following features:
+
+ - A Sublime Text build system for executing:
+ - `go build`
+ - `go install`
+ - `go test`
+ - `go clean`
+ - Cross-compilation using `go build` with `GOOS` and `GOARCH`
+ - Sublime Text command palette commands to:
+ - Execute `go get`
+ - Open a terminal into a Go workspace
+
+## Documentation
+
+### End User
+
+ - [Usage](usage.md)
+ - [Configuration](configuration.md)
+ - [Commands](commands.md)
+ - [Changelog](../changelog.md)
+ - [License](../LICENSE)
+ - [Patents](../PATENTS)
+
+### Contributor
+
+ - [Contributing](../CONTRIBUTING.md)
+ - [Design](design.md)
+ - [Development](development.md)
+ - [Contributors](../CONTRIBUTORS)
+ - [Authors](../AUTHORS)
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..13afcb8
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,75 @@
+# *Golang Build* Usage
+
+The primary functionality of the *Golang Build* package is the *Golang Build*
+build system. It includes a number of what Sublime Text refers to as "variants."
+It also includes a couple of regular Sublime Text commands for common, related
+tasks.
+
+ - [Build System](#build-system)
+ - [Other Commands](#other-commands)
+ - [Configuration](#configuration)
+ - [Commands](#commands)
+
+## Build System
+
+To use the *Golang Build* build system, open the *Tools > Build System* menu and
+select *Golang Build*.
+
+The variants included with the build system include:
+
+ - **Build**, which executes `go build`
+ - **Test**, which executes `go test`
+ - **Install**, which executes `go install`
+ - **Cross-Compile (Interactive)**, which executes `go build` with `GOOS` and
+ `GOARCH` set
+ - **Clean**, which executes `go clean`
+
+Once the *Golang Build* build system is selected, the command palette can be
+used to run any of the variants.
+
+On Sublime Text 3, the command palette entries will be:
+
+ - `Build with: Golang Build`
+ - `Build with: Golang Build - Test`
+ - `Build with: Golang Build - Install`
+ - `Build with: Golang Build - Cross-Compile (Interactive)`
+ - `Build with: Golang Build - Clean`
+
+On Sublime Text 2, the command palette entries will be:
+
+ - `Build: Build`
+ - `Build: Test`
+ - `Build: Install`
+ - `Build: Cross-Compile (Interactive)`
+ - `Build: Clean`
+
+### Cancelling a Build
+
+If a build is running and needs to be stopped, the command palette will contain
+an extra entry `Golang Build: Cancel`.
+
+### Reopening Build Results
+
+If the output panel for a build is closed, it can be re-opened by using the
+command palette to run `Golang Build: Reopen Output`. *Once a new build is
+started, the old build output is erased.*
+
+## Other Commands
+
+In addition to the build system variants, two other command palette commands are
+available:
+
+ - `Golang Build: Get`, which executes `go get` after prompting for a URL
+ - `Golang Build: Terminal`, which opens a terminal and sets relevant Go
+ environment variables
+
+## Configuration
+
+To control the environment variables used with the build system, please read
+the [configuration documentation](configuration.md).
+
+## Commands
+
+For information on the available commands, their arguments and example key
+bindings and command palette entries, please read the
+[commands documentation](commands.md).
diff --git a/golang_build.py b/golang_build.py
new file mode 100644
index 0000000..7062af9
--- /dev/null
+++ b/golang_build.py
@@ -0,0 +1,1021 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import sys
+import os
+import threading
+import subprocess
+import time
+import re
+import textwrap
+import collections
+
+if sys.version_info < (3,):
+ import Queue as queue
+else:
+ import queue
+
+import sublime
+import sublime_plugin
+
+import shellenv
+import golangconfig
+import newterm
+import package_events
+
+
+# A list of the environment variables to pull from settings when creating a
+# subprocess. Some subprocesses may have one or more manually overridden.
+GO_ENV_VARS = set([
+ 'GOPATH',
+ 'GOROOT',
+ 'GOROOT_FINAL',
+ 'GOBIN',
+ 'GOHOSTOS',
+ 'GOHOSTARCH',
+ 'GOOS',
+ 'GOARCH',
+ 'GOARM',
+ 'GO386',
+ 'GORACE',
+])
+
+# References to any existing GolangProcess() for a sublime.Window.id(). For
+# basic get and set operations, the dict is threadsafe.
+_PROCS = {}
+
+# References to any existing GolangPanel() for a sublime.Window.id(). For
+# basic get and set operations, the dict is threadsafe.
+_PANELS = {}
+_PANEL_LOCK = threading.Lock()
+
+
+class GolangBuildCommand(sublime_plugin.WindowCommand):
+
+ """
+ Command to run "go build", "go install", "go test" and "go clean"
+ """
+
+ def run(self, task='build', flags=None):
+ """
+ Runs the "golang_build" command - invoked by Sublime Text via the
+ command palette or sublime.Window.run_command()
+
+ :param task:
+ A unicode string of "build", "test", "install", "clean"
+ or "cross_compile"
+
+ :param flags:
+ A list of unicode strings of flags to send to the command-line go
+ tool. The "cross_compile" task executes the "build" command with
+ the GOOS and GOARCH environment variables set, meaning that
+ flags for "build" should be used with it. Execute "go help" on the
+ command line to learn about available flags.
+ """
+
+ if _yeild_to_running_build(self.window):
+ return
+
+ working_dir = _determine_working_dir(self.window)
+ if working_dir is None:
+ return
+
+ go_bin, env = _get_config(
+ 'go',
+ set(['GOPATH']),
+ GO_ENV_VARS - set(['GOPATH']),
+ view=self.window.active_view(),
+ window=self.window,
+ )
+ if (go_bin, env) == (None, None):
+ return
+
+ if flags is None:
+ flags, _ = golangconfig.setting_value(
+ '%s:flags' % task,
+ view=self.window.active_view(),
+ window=self.window
+ )
+
+ if task == 'cross_compile':
+ _task_cross_compile(
+ self,
+ go_bin,
+ flags,
+ working_dir,
+ env
+ )
+ return
+
+ args = [go_bin, task, '-v']
+ if flags and isinstance(flags, list):
+ args.extend(flags)
+ proc = _run_process(
+ task,
+ self.window,
+ args,
+ working_dir,
+ env
+ )
+ _set_proc(self.window, proc)
+
+
+def _task_cross_compile(command, go_bin, flags, working_dir, env):
+ """
+ Prompts the user to select the OS and ARCH to use for a cross-compile
+
+ :param command:
+ A sublime_plugin.WindowCommand object
+
+ :param go_bin:
+ A unicode string with the path to the "go" executable
+
+ :param flags:
+ A list of unicode string of flags to pass to the "go" executable
+
+ :param working_dir:
+ A unicode string with the working directory for the "go" executable
+
+ :param env:
+ A dict of environment variables to use with the "go" executable
+ """
+
+ valid_combinations = [
+ ('darwin', '386'),
+ ('darwin', 'amd64'),
+ ('darwin', 'arm'),
+ ('darwin', 'arm64'),
+ ('dragonfly', 'amd64'),
+ ('freebsd', '386'),
+ ('freebsd', 'amd64'),
+ ('freebsd', 'arm'),
+ ('linux', '386'),
+ ('linux', 'amd64'),
+ ('linux', 'arm'),
+ ('linux', 'arm64'),
+ ('linux', 'ppc64'),
+ ('linux', 'ppc64le'),
+ ('netbsd', '386'),
+ ('netbsd', 'amd64'),
+ ('netbsd', 'arm'),
+ ('openbsd', '386'),
+ ('openbsd', 'amd64'),
+ ('openbsd', 'arm'),
+ ('plan9', '386'),
+ ('plan9', 'amd64'),
+ ('solaris', 'amd64'),
+ ('windows', '386'),
+ ('windows', 'amd64'),
+ ]
+
+ def on_done(index):
+ """
+ Processes the user's input and launch the build process
+
+ :param index:
+ The index of the option the user selected, or -1 if cancelled
+ """
+
+ if index == -1:
+ return
+
+ env['GOOS'], env['GOARCH'] = valid_combinations[index]
+
+ args = [go_bin, 'build', '-v']
+ if flags and isinstance(flags, list):
+ args.extend(flags)
+ proc = _run_process(
+ 'cross_compile',
+ command.window,
+ args,
+ working_dir,
+ env
+ )
+ _set_proc(command.window, proc)
+
+ quick_panel_options = []
+ for os_, arch in valid_combinations:
+ quick_panel_options.append('OS: %s, ARCH: %s' % (os_, arch))
+
+ command.window.show_quick_panel(
+ quick_panel_options,
+ on_done
+ )
+
+
+class GolangBuildCancelCommand(sublime_plugin.WindowCommand):
+
+ """
+ Terminates any existing "go" process that is running for the current window
+ """
+
+ def run(self):
+ proc = _get_proc(self.window)
+ if proc and not proc.finished:
+ proc.terminate()
+ if proc is not None:
+ _set_proc(self.window, None)
+
+ def is_enabled(self):
+ proc = _get_proc(self.window)
+ if not proc:
+ return False
+ return not proc.finished
+
+
+class GolangBuildReopenCommand(sublime_plugin.WindowCommand):
+
+ """
+ Reopens the output from the last build command
+ """
+
+ def run(self):
+ self.window.run_command('show_panel', {'panel': 'output.golang_build'})
+
+
+class GolangBuildGetCommand(sublime_plugin.WindowCommand):
+
+ """
+ Prompts the use to enter the URL of a Go package to get
+ """
+
+ def run(self, url=None, flags=None):
+ """
+ Runs the "golang_build_get" command - invoked by Sublime Text via the
+ command palette or sublime.Window.run_command()
+
+ :param url:
+ A unicode string of the URL to download, instead of prompting the
+ user
+
+ :param flags:
+ A list of unicode strings of flags to send to the command-line go
+ tool. Execute "go help" on the command line to learn about available
+ flags.
+ """
+
+ if _yeild_to_running_build(self.window):
+ return
+
+ working_dir = _determine_working_dir(self.window)
+ if working_dir is None:
+ return
+
+ go_bin, env = _get_config(
+ 'go',
+ set(['GOPATH']),
+ GO_ENV_VARS - set(['GOPATH']),
+ view=self.window.active_view(),
+ window=self.window,
+ )
+ if (go_bin, env) == (None, None):
+ return
+
+ if flags is None:
+ flags, _ = golangconfig.setting_value(
+ 'get:flags',
+ view=self.window.active_view(),
+ window=self.window
+ )
+
+ def on_done(get_url):
+ """
+ Processes the user's input and launches the "go get" command
+
+ :param get_url:
+ A unicode string of the URL to get
+ """
+
+ args = [go_bin, 'get', '-v']
+ if flags and isinstance(flags, list):
+ args.extend(flags)
+ args.append(get_url)
+ proc = _run_process(
+ 'get',
+ self.window,
+ args,
+ working_dir,
+ env
+ )
+ _set_proc(self.window, proc)
+
+ if url is not None:
+ on_done(url)
+ return
+
+ self.window.show_input_panel(
+ 'go get',
+ '',
+ on_done,
+ None,
+ None
+ )
+
+
+class GolangBuildTerminalCommand(sublime_plugin.WindowCommand):
+
+ """
+ Opens a terminal for the user to the directory containing the open file,
+ setting any necessary environment variables
+ """
+
+ def run(self):
+ """
+ Runs the "golang_build_terminal" command - invoked by Sublime Text via
+ the command palette or sublime.Window.run_command()
+ """
+
+ working_dir = _determine_working_dir(self.window)
+ if working_dir is None:
+ return
+
+ relevant_sources = set([
+ 'project file',
+ 'project file (os-specific)',
+ 'golang.sublime-settings',
+ 'golang.sublime-settings (os-specific)'
+ ])
+
+ env_overrides = {}
+ for var_name in GO_ENV_VARS:
+ value, source = golangconfig.setting_value(var_name, window=self.window)
+ # Only set overrides that are not coming from the user's shell
+ if source in relevant_sources:
+ env_overrides[var_name] = value
+
+ # Get the PATH from the shell environment and then prepend any custom
+ # value so the user's terminal searches all locations
+ value, source = golangconfig.setting_value('PATH', window=self.window)
+ if source in relevant_sources:
+ shell, env = shellenv.get_env()
+ env_overrides['PATH'] = value + os.pathsep + env.get('PATH', '')
+
+ newterm.launch_terminal(working_dir, env=env_overrides)
+
+
+def _yeild_to_running_build(window):
+ """
+ Check if a build is already running, and if so, allow the user to stop it,
+ or cancel the new build
+
+ :param window:
+ A sublime.Window of the window the build is being run in
+
+ :return:
+ A boolean - if the new build should be abandoned
+ """
+
+ proc = _get_proc(window)
+ if proc and not proc.finished:
+ message = _format_message("""
+ Golang Build
+
+ There is already a build running. Would you like to stop it?
+ """)
+ if not sublime.ok_cancel_dialog(message, 'Stop Running Build'):
+ return True
+ proc.terminate()
+ _set_proc(window, None)
+
+ return False
+
+
+def _determine_working_dir(window):
+ """
+ Determine the working directory for a command based on the user's open file
+ or open folders
+
+ :param window:
+ The sublime.Window object of the window the command was run on
+
+ :return:
+ A unicode string of the working directory, or None if no working
+ directory was found
+ """
+
+ view = window.active_view()
+ working_dir = None
+
+ # If a file is open, get the folder from the file, and error if the file
+ # has not been saved yet
+ if view:
+ if view.file_name():
+ working_dir = os.path.dirname(view.file_name())
+
+ # If no file is open, then get the list of folders and grab the first one
+ else:
+ folders = window.folders()
+ if len(folders) > 0:
+ working_dir = folders[0]
+
+ if working_dir is None or not os.path.exists(working_dir):
+ message = _format_message("""
+ Golang Build
+
+ No files or folders are open, or the open file or folder does not exist on disk
+ """)
+ sublime.error_message(message)
+ return None
+
+ return working_dir
+
+
+def _get_config(executable_name, required_vars, optional_vars=None, view=None, window=None):
+ """
+ :param executable_name:
+ A unicode string of the executable to locate, e.g. "go" or "gofmt"
+
+ :param required_vars:
+ A list of unicode strings of the environment variables that are
+ required, e.g. "GOPATH". Obtains values from setting_value().
+
+ :param optional_vars:
+ A list of unicode strings of the environment variables that are
+ optional, but should be pulled from setting_value() if available - e.g.
+ "GOOS", "GOARCH". Obtains values from setting_value().
+
+ :param view:
+ A sublime.View object to use in finding project-specific settings. This
+ should be passed whenever available.
+
+ :param window:
+ A sublime.Window object to use in finding project-specific settings.
+ This will only work for Sublime Text 3, and should only be passed if
+ no sublime.View object is available to pass via the view parameter.
+
+ :return:
+ A two-element tuple.
+
+ If there was an error finding the executable or required vars:
+
+ - [0] None
+ - [1] None
+
+ Otherwise:
+
+ - [0] A string of the path to the executable
+ - [1] A dict of environment variables for the executable
+ """
+
+ try:
+ return golangconfig.subprocess_info(
+ executable_name,
+ required_vars,
+ optional_vars,
+ view=view,
+ window=window
+ )
+
+ except (golangconfig.ExecutableError) as e:
+ error_message = '''
+ Golang Build
+
+ The %s executable could not be found. Please ensure it is
+ installed and available via your PATH.
+
+ Would you like to view documentation for setting a custom PATH?
+ '''
+
+ prompt = error_message % e.name
+
+ if sublime.ok_cancel_dialog(_format_message(prompt), 'Open Documentation'):
+ window.run_command(
+ 'open_url',
+ {'url': 'https://go.googlesource.com/sublime-build/+/master/docs/configuration.md'}
+ )
+
+ except (golangconfig.EnvVarError) as e:
+ error_message = '''
+ Golang Build
+
+ The setting%s %s could not be found in your Sublime Text
+ settings or your shell environment.
+
+ Would you like to view the configuration documentation?
+ '''
+
+ plural = 's' if len(e.missing) > 1 else ''
+ setting_names = ', '.join(e.missing)
+ prompt = error_message % (plural, setting_names)
+
+ if sublime.ok_cancel_dialog(_format_message(prompt), 'Open Documentation'):
+ window.run_command(
+ 'open_url',
+ {'url': 'https://go.googlesource.com/sublime-build/+/master/docs/configuration.md'}
+ )
+
+ return (None, None)
+
+
+class GolangProcess():
+
+ """
+ A wrapper around subprocess.Popen() that provides information about how
+ the process was started and finished, plus a queue.Queue of output
+ """
+
+ # A float of the unix timestamp of when the process was started
+ started = None
+
+ # A list of strings (unicode for Python 3, byte string for Python 2) of
+ # the process path and any arguments passed to it
+ args = None
+
+ # A unicode string of the process working directory
+ cwd = None
+
+ # A dict of the env passed to the process
+ env = None
+
+ # A subprocess.Popen() object of the running process
+ proc = None
+
+ # A queue.Queue object of output from the process
+ output = None
+
+ # The result of the process, a unicode string of "cancelled", "success" or "error"
+ result = None
+
+ # A float of the unix timestamp of when the process ended
+ finished = None
+
+ # A threading.Lock() used to prevent the stdout and stderr handlers from
+ # both trying to perform process cleanup at the same time
+ _cleanup_lock = None
+
+ def __init__(self, args, cwd, env):
+ """
+ :param args:
+ A list of strings (unicode for Python 3, byte string for Python 2)
+ of the process path and any arguments passed to it
+
+ :param cwd:
+ A unicode string of the working directory for the process
+
+ :param env:
+ A dict of strings (unicode for Python 3, byte string for Python 2)
+ to pass to the process as the environment variables
+ """
+
+ self.args = args
+ self.cwd = cwd
+ self.env = env
+
+ startupinfo = None
+ if sys.platform == 'win32':
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+
+ self._cleanup_lock = threading.Lock()
+ self.started = time.time()
+ self.proc = subprocess.Popen(
+ args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ cwd=cwd,
+ env=env,
+ startupinfo=startupinfo
+ )
+ self.finished = False
+
+ self.output = queue.Queue()
+
+ self._stdout_thread = threading.Thread(
+ target=self._read_output,
+ args=(
+ self.output,
+ self.proc.stdout.fileno(),
+ 'stdout'
+ )
+ )
+ self._stdout_thread.start()
+
+ self._stderr_thread = threading.Thread(
+ target=self._read_output,
+ args=(
+ self.output,
+ self.proc.stderr.fileno(),
+ 'stderr'
+ )
+ )
+ self._stderr_thread.start()
+
+ self._cleanup_thread = threading.Thread(target=self._cleanup)
+ self._cleanup_thread.start()
+
+ def wait(self):
+ """
+ Blocks waiting for the subprocess to complete
+ """
+
+ self._cleanup_thread.wait()
+
+ def terminate(self):
+ """
+ Terminates the subprocess
+ """
+
+ self._cleanup_lock.acquire()
+ try:
+ if not self.proc:
+ return
+ self.proc.terminate()
+ self.result = 'cancelled'
+ self.finished = time.time()
+ self.proc = None
+ finally:
+ self._cleanup_lock.release()
+
+ def _read_output(self, output_queue, fileno, output_type):
+ """
+ Handler to process output from stdout/stderr
+
+ RUNS IN A THREAD
+
+ :param output_queue:
+ The queue.Queue object to add the output to
+
+ :param fileno:
+ The fileno to read output from
+
+ :param output_type:
+ A unicode string of "stdout" or "stderr"
+ """
+
+ while self.proc and self.proc.poll() is None:
+ chunk = os.read(fileno, 32768)
+ if len(chunk) == 0:
+ break
+ output_queue.put((output_type, chunk.decode('utf-8')))
+
+ def _cleanup(self):
+ """
+ Cleans up the subprocess and marks the state of self appropriately
+
+ RUNS IN A THREAD
+ """
+
+ self._stdout_thread.join()
+ self._stderr_thread.join()
+
+ self._cleanup_lock.acquire()
+ try:
+ if not self.proc:
+ return
+ # Get the returncode to prevent a zombie/defunct child process
+ self.proc.wait()
+ self.result = 'success' if self.proc.returncode == 0 else 'error'
+ self.finished = time.time()
+ self.proc = None
+ finally:
+ self._cleanup_lock.release()
+ self.output.put(('eof', None))
+
+
+class GolangProcessPrinter():
+
+ """
+ Describes a Go process, the environment it was started in and its result
+ """
+
+ # The GolangProcess() object the printer is displaying output from
+ proc = None
+
+ # The GolangPanel() object the information is written to
+ panel = None
+
+ def __init__(self, proc, panel):
+ """
+ :param proc:
+ A GolangProcess() object
+
+ :param panel:
+ A GolangPanel() object to write information to
+ """
+
+ self.proc = proc
+ self.panel = panel
+
+ self.thread = threading.Thread(
+ target=self._run
+ )
+ self.thread.start()
+
+ def _run(self):
+ """
+ GolangProcess() output queue processor
+
+ RUNS IN A THREAD
+ """
+
+ self.panel.printer_lock.acquire()
+
+ try:
+ self._write_header()
+
+ while True:
+ message_type, message = self.proc.output.get()
+
+ if message_type == 'eof':
+ break
+
+ if message_type == 'stdout':
+ output = message
+
+ if message_type == 'stderr':
+ output = message
+
+ self.panel.write(output)
+
+ self._write_footer()
+
+ finally:
+ self.panel.printer_lock.release()
+
+ def _write_header(self):
+ """
+ Displays startup information about the process
+ """
+
+ title = ''
+
+ env_vars = []
+ for var_name in GO_ENV_VARS:
+ var_key = var_name if sys.version_info >= (3,) else var_name.encode('ascii')
+ if var_key in self.proc.env:
+ value = self.proc.env.get(var_key)
+ if sys.version_info < (3,):
+ value = value.decode('utf-8')
+ env_vars.append((var_name, value))
+ if env_vars:
+ title += '> Environment:\n'
+ for var_name, value in env_vars:
+ title += '> %s=%s\n' % (var_name, value)
+
+ title += '> Directory: %s\n' % self.proc.cwd
+ title += '> Command: %s\n' % subprocess.list2cmdline(self.proc.args)
+ title += '> Output:\n'
+
+ self.panel.write(title, content_separator='\n\n')
+
+ def _write_footer(self):
+ """
+ Displays result information about the process, blocking until the
+ write is completed
+ """
+
+ formatted_result = self.proc.result.title()
+ runtime = self.proc.finished - self.proc.started
+
+ output = '> Elapsed: %0.3fs\n> Result: %s' % (runtime, formatted_result)
+
+ event = threading.Event()
+ self.panel.write(output, content_separator='\n', event=event)
+ event.wait()
+
+ package_events.notify(
+ 'Golang Build',
+ 'build_complete',
+ BuildCompleteEvent(
+ task='',
+ args=list(self.proc.args),
+ working_dir=self.proc.cwd,
+ env=self.proc.env.copy(),
+ runtime=runtime,
+ result=self.proc.result
+ )
+ )
+
+
+BuildCompleteEvent = collections.namedtuple(
+ 'BuildCompleteEvent',
+ [
+ 'task',
+ 'args',
+ 'working_dir',
+ 'env',
+ 'runtime',
+ 'result',
+ ]
+)
+
+
+class GolangPanel():
+
+ """
+ Holds a reference to an output panel used by the Golang Build package,
+ and provides synchronization features to ensure output is printed in proper
+ order
+ """
+
+ # A sublime.View object of the output panel being printed to
+ panel = None
+
+ # A queue.Queue() that holds all of the info to be written to the panel
+ queue = None
+
+ # A lock used to ensure only on GolangProcessPrinter() is using the panel
+ # at any given time
+ printer_lock = None
+
+ def __init__(self, window):
+ """
+ :param window:
+ The sublime.Window object the output panel is contained within
+ """
+
+ self.printer_lock = threading.Lock()
+ self.reset(window)
+
+ def reset(self, window):
+ """
+ Creates a new, fresh output panel and output Queue object
+
+ :param window:
+ The sublime.Window object the output panel is contained within
+ """
+
+ if not isinstance(threading.current_thread(), threading._MainThread):
+ raise RuntimeError('GolangPanel.reset() must be run in the UI thread')
+
+ self.queue = queue.Queue()
+ self.panel = window.get_output_panel('golang_build')
+
+ st_settings = sublime.load_settings('Preferences.sublime-settings')
+ panel_settings = self.panel.settings()
+ panel_settings.set('syntax', 'Packages/Golang Build/Golang Build Output.tmLanguage')
+ panel_settings.set('color_scheme', st_settings.get('color_scheme'))
+ panel_settings.set('draw_white_space', 'selection')
+ panel_settings.set('word_wrap', False)
+ panel_settings.set("auto_indent", False)
+ panel_settings.set('line_numbers', False)
+ panel_settings.set('gutter', False)
+ panel_settings.set('scroll_past_end', False)
+
+ def write(self, string, content_separator=None, event=None):
+ """
+ Queues data to be written to the output panel. Normally this will be
+ called from a thread other than the UI thread.
+
+ :param string:
+ A unicode string to write to the output panel
+
+ :param content_separator:
+ A unicode string to prefix to the string param if there is already
+ output in the output panel. Is only prefixed if the previous number
+ of characters are not equal to this string.
+
+ :param event:
+ An optional threading.Event() object to set once the data has been
+ written to the output panel
+ """
+
+ self.queue.put((string, content_separator, event))
+ sublime.set_timeout(self._process_queue, 1)
+
+ def _process_queue(self):
+ """
+ A callback that is run in the UI thread to actually perform writes to
+ the output panel. Reads from the queue until it is empty.
+ """
+
+ try:
+ while True:
+ chars, content_separator, event = self.queue.get(False)
+
+ if content_separator is not None and self.panel.size() > 0:
+ end = self.panel.size()
+ start = end - len(content_separator)
+ if self.panel.substr(sublime.Region(start, end)) != content_separator:
+ chars = content_separator + chars
+
+ # In Sublime Text 2, the "insert" command does not handle newlines
+ if sys.version_info < (3,):
+ edit = self.panel.begin_edit('golang_panel_print', [])
+ self.panel.insert(edit, self.panel.size(), chars)
+ self.panel.end_edit(edit)
+
+ else:
+ self.panel.run_command('insert', {'characters': chars})
+
+ if event:
+ event.set()
+
+ except (queue.Empty):
+ pass
+
+
+def _run_process(task, window, args, cwd, env):
+ """
+ Starts a GolangProcess() and creates a GolangProcessPrinter() for it
+
+ :param task:
+ A unicode string of the build task name - one of "build", "test",
+ "cross_compile", "install", "clean", "get"
+
+ :param window:
+ A sublime.Window object of the window to display the output panel in
+
+ :param args:
+ A list of strings (unicode for Python 3, byte string for Python 2)
+ of the process path and any arguments passed to it
+
+ :param cwd:
+ A unicode string of the working directory for the process
+
+ :param env:
+ A dict of strings (unicode for Python 3, byte string for Python 2)
+ to pass to the process as the environment variables
+
+ :return:
+ A GolangProcess() object
+ """
+
+ panel = _get_panel(window)
+
+ proc = GolangProcess(args, cwd, env)
+
+ # If there is no printer using the panel, reset it
+ if panel.printer_lock.acquire(False):
+ panel.reset(window)
+ panel.printer_lock.release()
+
+ GolangProcessPrinter(proc, panel)
+
+ window.run_command('show_panel', {'panel': 'output.golang_build'})
+
+ return proc
+
+
+def _set_proc(window, proc):
+ """
+ Sets the GolangProcess() object associated with a sublime.Window
+
+ :param window:
+ A sublime.Window object
+
+ :param proc:
+ A GolangProcess() object that is being run for the window
+ """
+
+ _PROCS[window.id()] = proc
+
+
+def _get_proc(window):
+ """
+ Returns the GolangProcess() object associated with a sublime.Window
+
+ :param window:
+ A sublime.Window object
+
+ :return:
+ None or a GolangProcess() object. The GolangProcess() may or may not
+ still be running.
+ """
+
+ return _PROCS.get(window.id())
+
+
+def _get_panel(window):
+ """
+ Returns the GolangPanel() object associated with a sublime.Window
+
+ :param window:
+ A sublime.Window object
+
+ :return:
+ A GolangPanel() object
+ """
+
+ _PANEL_LOCK.acquire()
+ try:
+ if window.id() not in _PANELS:
+ _PANELS[window.id()] = GolangPanel(window)
+ return _PANELS.get(window.id())
+ finally:
+ _PANEL_LOCK.release()
+
+
+def _format_message(string):
+ """
+ Takes a multi-line string and does the following:
+
+ - dedents
+ - converts newlines with text before and after into a single line
+ - strips leading and trailing whitespace
+
+ :param string:
+ The string to format
+
+ :return:
+ The formatted string
+ """
+
+ output = textwrap.dedent(string)
+
+ # Unwrap lines, taking into account bulleted lists, ordered lists and
+ # underlines consisting of = signs
+ if output.find('\n') != -1:
+ output = re.sub('(?<=\\S)\n(?=[^ \n\t\\d\\*\\-=])', ' ', output)
+
+ return output.strip()
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..5ab1173
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,45 @@
+# Golang Build
+
+*Golang Build* is a Sublime Text package for compiling Go projects. It provides
+integration between Sublime Text and the command line `go` tool.
+
+The package consists of the following features:
+
+ - A Sublime Text build system for executing:
+ - `go build`
+ - `go install`
+ - `go test`
+ - `go clean`
+ - Cross-compilation using `go build` with `GOOS` and `GOARCH`
+ - Sublime Text command palette commands to:
+ - Execute `go get`
+ - Open a terminal into a Go workspace
+
+## Installation
+
+The *Golang Build* package is installed by using
+[Package Control](https://packagecontrol.io).
+
+ - If Package Control is not installed, follow the [Installation Instructions](https://packagecontrol.io/installation)
+ - Open the Sublime Text command palette and run the `Package Control: Install
+ Package` command
+ - Type `Golang Build` and select the package to perform the installation
+
+## Documentation
+
+### End User
+
+ - [Usage](docs/usage.md)
+ - [Configuration](docs/configuration.md)
+ - [Commands](docs/commands.md)
+ - [Changelog](changelog.md)
+ - [License](LICENSE)
+ - [Patents](PATENTS)
+
+### Contributor
+
+ - [Contributing](CONTRIBUTING.md)
+ - [Design](docs/design.md)
+ - [Development](docs/development.md)
+ - [Contributors](CONTRIBUTORS)
+ - [Authors](AUTHORS)