diff --git a/.gitignore b/.gitignore index 6fa0df40e..e5a73f925 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ +src/deps/* src/deps/*/* src/*.o src/howl +src/howl.exe +src/howl.ico +src/howl.png +src/howl.res +src/windist +src/wininst *.bc *.bak site/build/ diff --git a/README.md b/README.md index 7514f24f0..7933f33a9 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ how to install Howl from source. based system). - `C compiler`: Howl has a very small C core itself, and it embeds dependencies written in C. +- If you're using Windows, you *need* to build Howl inside of +[MSYS2](https://msys2.github.io/)'s MinGW32 shell. ### Build && install diff --git a/bundles/git/spec/git_spec.moon b/bundles/git/spec/git_spec.moon index ab45d5da0..6ca50d21f 100644 --- a/bundles/git/spec/git_spec.moon +++ b/bundles/git/spec/git_spec.moon @@ -1,6 +1,11 @@ import bundle, config, VC from howl import File from howl.io +echo = if jit.os == 'Windows' + "#{howl.sys.env.WD}echo.exe" +else + '/bin/echo' + describe 'Git bundle', -> setup -> bundle.load_by_name 'git' teardown -> bundle.unload 'git' @@ -26,8 +31,7 @@ describe 'Git bundle', -> assert.equal instance.root, dir it 'returns nil if no git root was found', -> - File.with_tmpfile (file) -> - assert.is_nil git_vc.find file + assert.is_nil git_vc.find howl.io.File echo describe 'A Git instance', -> root = nil @@ -106,7 +110,7 @@ describe 'Git bundle', -> it 'uses the executable in variable `git_path` if specified', (done) -> howl_async -> - config.git_path = '/bin/echo' + config.git_path = echo status, out = pcall git.run, git, 'using echo' config.git_path = nil assert status, out diff --git a/bundles/howl-themes/solarized_light/solarized_light.moon b/bundles/howl-themes/solarized_light/solarized_light.moon index 9c7cc4d6e..ee225f041 100644 --- a/bundles/howl-themes/solarized_light/solarized_light.moon +++ b/bundles/howl-themes/solarized_light/solarized_light.moon @@ -263,7 +263,7 @@ return { font: bold: true size: 'large' - family: 'Purisa,Latin Modern Sans' + family: 'Purisa,Comic Sans,Latin Modern Sans,Lucida Sans Unicode' definition: color: yellow function: color: blue diff --git a/bundles/howl-themes/steinom/steinom.moon b/bundles/howl-themes/steinom/steinom.moon index 70723435f..b054ac963 100644 --- a/bundles/howl-themes/steinom/steinom.moon +++ b/bundles/howl-themes/steinom/steinom.moon @@ -247,7 +247,7 @@ return { font: bold: true size: 'large' - family: 'Purisa,Latin Modern Sans' + family: 'Purisa,Comic Sans,Latin Modern Sans,Lucida Sans Unicode' definition: color: yellow diff --git a/bundles/howl-themes/tomorrow_night_blue/tm_night_blue.moon b/bundles/howl-themes/tomorrow_night_blue/tm_night_blue.moon index 4dde9709b..d715d1c7f 100644 --- a/bundles/howl-themes/tomorrow_night_blue/tm_night_blue.moon +++ b/bundles/howl-themes/tomorrow_night_blue/tm_night_blue.moon @@ -246,7 +246,7 @@ return { font: bold: true size: 'large' - family: 'Purisa,Latin Modern Sans' + family: 'Purisa,Comic Sans,Latin Modern Sans,Lucida Sans Unicode' definition: color: yellow diff --git a/lib/aullar/gutter.moon b/lib/aullar/gutter.moon index 81b82a81a..e2a8760e2 100644 --- a/lib/aullar/gutter.moon +++ b/lib/aullar/gutter.moon @@ -4,7 +4,7 @@ {:define_class} = require 'aullar.util' Pango = require 'ljglibs.pango' {:RGBA} = require 'ljglibs.gdk' -Background = require 'ljglibs.aux.background' +Background = require 'ljglibs.util.background' Layout = Pango.Layout pango_cairo = Pango.cairo diff --git a/lib/aullar/view.moon b/lib/aullar/view.moon index 046502e79..23e33ff65 100644 --- a/lib/aullar/view.moon +++ b/lib/aullar/view.moon @@ -282,9 +282,14 @@ View = { if opts.vertical page_size = @lines_showing - 1 + upper = @buffer.nr_lines + if page_size == 0 + -- GtkRange docs mention that page_size is normally >0 for GtkScrollbar + upper = 2 + page_size = 1 adjustment = @vertical_scrollbar.adjustment if adjustment - adjustment\configure @first_visible_line, 1, @buffer.nr_lines, 1, page_size, page_size + adjustment\configure @first_visible_line, 1, upper, 1, page_size, page_size if opts.horizontal max_width = 0 diff --git a/lib/howl/aux.moon b/lib/howl/aux.moon deleted file mode 100644 index d83b665e6..000000000 --- a/lib/howl/aux.moon +++ /dev/null @@ -1,9 +0,0 @@ -util = howl.util - -setmetatable {}, { - __index: (key) => - tb = debug.traceback '', 2 - first = tb\match 'stack traceback:\n%s*([^\n]+)' - log.error "`howl.aux` is deprecated, please update your code to use `howl.util` instead\n (#{first})\n" - util[key] -} diff --git a/lib/howl/cdefs/init.moon b/lib/howl/cdefs/init.moon index f16dad85d..98ca6aa84 100644 --- a/lib/howl/cdefs/init.moon +++ b/lib/howl/cdefs/init.moon @@ -51,6 +51,9 @@ int sig_PWR; int sig_SYS; ]] +if ffi.os == 'Windows' + require 'howl.cdefs.windows' + return { const_char_p: ffi.typeof 'const char *' char_p: ffi.typeof 'char *' diff --git a/lib/howl/cdefs/windows.moon b/lib/howl/cdefs/windows.moon new file mode 100644 index 000000000..76da6222d --- /dev/null +++ b/lib/howl/cdefs/windows.moon @@ -0,0 +1,21 @@ +-- Copyright 2016 The Howl Developers +-- License: MIT (see LICENSE.md at the top-level directory of the distribution) + +ffi = require 'ffi' + +ffi.cdef [[ + typedef void* HANDLE; + typedef void* PVOID; + typedef int DWORD; + typedef int BOOL; + typedef char* LPTSTR; + typedef const char* LPCTSTR; + + DWORD GetProcessId(HANDLE process); + HANDLE _get_osfhandle(int fd); + BOOL TerminateProcess(HANDLE process, unsigned int exitcode); + int AddFontResourceExA(LPCTSTR lpszFilename, DWORD fl, PVOID pdv); + BOOL SetFileAttributesA(LPCTSTR file, DWORD attrs); + + int fr_private; +]] diff --git a/lib/howl/commands/app_commands.moon b/lib/howl/commands/app_commands.moon index 8bced976a..ca8c2995e 100644 --- a/lib/howl/commands/app_commands.moon +++ b/lib/howl/commands/app_commands.moon @@ -359,7 +359,7 @@ command.register ----------------------------------------------------------------------- launch_cmd = (working_directory, cmd) -> - shell = howl.sys.env.SHELL or '/bin/sh' + shell = howl.sys.env.SHELL p = Process { :cmd, :shell, diff --git a/lib/howl/init.lua b/lib/howl/init.lua index 3d39a0c8f..489484281 100644 --- a/lib/howl/init.lua +++ b/lib/howl/init.lua @@ -20,6 +20,8 @@ Where options can be any of: ]=] local path_separator = jit.os == 'Windows' and '\\' or '/' +local path_prefix = jit.os == 'Windows' and '\\\\.\\' or '' +app_root = path_prefix .. app_root local function parse_args(arg_vector) local options = { @@ -168,12 +170,14 @@ local function main() set_package_path('lib', 'lib/ext', 'lib/ext/moonscript') require 'howl.moonscript_support' table.insert(package.loaders, 2, bytecode_loader()) - require 'howl.cdefs.fontconfig' - ffi.C.FcConfigAppFontAddDir(nil, table.concat({app_root, 'fonts'}, path_separator)) require 'ljglibs.cdefs.glib' howl = auto_module('howl') require('howl.globals') + + local font_dir = table.concat({app_root, 'fonts'}, path_separator) + require('howl.sys').platform.load_font_dir(font_dir) + _G.log = require('howl.log') local args = parse_args(argv) diff --git a/lib/howl/interactions/file_selection.moon b/lib/howl/interactions/file_selection.moon index bffbeea23..259c034bb 100644 --- a/lib/howl/interactions/file_selection.moon +++ b/lib/howl/interactions/file_selection.moon @@ -45,13 +45,15 @@ class FileSelector parent or= File.home_dir path = @command_line\pop_spillover! + -- Make / work on Windows. + path = path\gsub '/', File.separator if path.is_empty - path = tostring(parent) .. '/' + path = tostring(parent) .. File.separator else - trailing = path\ends_with('/') and '/' or '' + trailing = path\ends_with(File.separator) and File.separator or '' path = tostring parent / path - if not path\ends_with '/' + if not path\ends_with File.separator path ..= trailing directory, unmatched = get_dir_and_leftover path @@ -93,7 +95,12 @@ class FileSelector on_update: (text) => return if @submitting - path = @directory.path .. '/' .. text + -- This allows one to use / as a directory separator, even on Windows. + text = text\gsub '/', File.separator + path = if File.is_absolute text + text + else + @directory.path .. File.separator .. text directory, text = get_dir_and_leftover path if directory != @directory diff --git a/lib/howl/io/file.moon b/lib/howl/io/file.moon index 7c9fd6b7e..d74d49c14 100644 --- a/lib/howl/io/file.moon +++ b/lib/howl/io/file.moon @@ -5,6 +5,7 @@ GFile = require 'ljglibs.gio.file' GFileInfo = require 'ljglibs.gio.file_info' glib = require 'ljglibs.glib' import PropertyObject from howl.util.moon +import platform from howl.sys append = table.insert file_types = { @@ -19,12 +20,12 @@ file_types = { class File extends PropertyObject tmpfile: -> - file = File assert os.tmpname! - file.touch if not file.exists + file = File platform.tmpname! + file\touch! if not file.exists file tmpdir: -> - with File os.tmpname! + with File platform.tmpname! \delete! if .exists \mkdir! @@ -35,7 +36,7 @@ class File extends PropertyObject error err if not status is_absolute: (path) -> - (path\match('^/') or path\match('^%a:\\\\')) != nil + (path\match('^/') or path\match('^%a:\\')) != nil expand_path: (path) -> res = path\gsub "~#{File.separator}", File.home_dir.path .. File.separator @@ -79,7 +80,7 @@ class File extends PropertyObject @property size: get: => @_info!.size @property exists: get: => @gfile.exists @property readable: get: => @exists and @_info('access')\get_attribute_boolean 'access::can-read' - @property etag: get: => @exists and @_info('etag').etag + @property etag: get: => @exists and @_info('etag').etag or nil @property modified_at: get: => @exists and @_info('time')\get_attribute_uint64 'time::modified' @property short_path: get: => @path\gsub "^#{File.home_dir.path}", '~' diff --git a/lib/howl/io/input_stream.moon b/lib/howl/io/input_stream.moon index e96eba362..36d7c1773 100644 --- a/lib/howl/io/input_stream.moon +++ b/lib/howl/io/input_stream.moon @@ -3,13 +3,15 @@ dispatch = howl.dispatch glib = require 'ljglibs.glib' -{:UnixInputStream} = require 'ljglibs.gio' +{:Win32InputStream, :UnixInputStream} = require 'ljglibs.gio' {:PropertyObject} = howl.util.moon +{:platform} = howl.sys append = table.insert class InputStream extends PropertyObject new: (@stream, @priority = glib.PRIORITY_LOW) => - @stream = UnixInputStream(@stream) if type(@stream) == 'number' + if type(@stream) == 'number' + @stream = platform.fd_to_stream Win32InputStream, UnixInputStream, @stream super! @property is_closed: get: => @stream.is_closed diff --git a/lib/howl/io/output_stream.moon b/lib/howl/io/output_stream.moon index 8b349dd3d..3ce9f2250 100644 --- a/lib/howl/io/output_stream.moon +++ b/lib/howl/io/output_stream.moon @@ -2,12 +2,13 @@ -- License: MIT (see LICENSE.md at the top-level directory of the distribution) dispatch = howl.dispatch -{:UnixOutputStream} = require 'ljglibs.gio' +{:Win32OutputStream, :UnixOutputStream} = require 'ljglibs.gio' {:PropertyObject} = howl.util.moon +{:platform} = howl.sys class OutputStream extends PropertyObject new: (fd) => - @stream = UnixOutputStream fd + @stream = platform.fd_to_stream Win32OutputStream, UnixOutputStream, fd super! @property is_closed: get: => @stream.is_closed diff --git a/lib/howl/io/process.moon b/lib/howl/io/process.moon index 410477617..bb5d9effd 100644 --- a/lib/howl/io/process.moon +++ b/lib/howl/io/process.moon @@ -7,6 +7,7 @@ jit = require 'jit' callbacks = require 'ljglibs.callbacks' dispatch = howl.dispatch {:File, :InputStream, :OutputStream} = howl.io +{:platform} = howl.sys C, ffi_cast = ffi.C, ffi.cast append = table.insert @@ -36,11 +37,16 @@ shell_quote = (s) -> else s -get_command = (v, shell = '/bin/sh') -> +get_command = (v, shell = platform.default_shell!) -> t = type v if t == 'string' - return { shell, '-c', v }, v + arg = if shell\find 'cmd.exe' + -- Likely cmd.exe. + '/C' + else + '-c' + return { shell, arg, v }, v elseif t != 'table' return nil @@ -108,6 +114,7 @@ class Process @argv, @command_line = get_command opts.cmd, opts.shell error 'opts.cmd missing or invalid', 2 unless @argv @_process = launch @argv, opts + @true_pid = @_process.true_pid @pid = @_process.pid @working_directory = File opts.working_directory or get_current_dir! @stdin = OutputStream(@_process.stdin_pipe) if @_process.stdin_pipe @@ -118,7 +125,7 @@ class Process @@running[@pid] = @ @_exit_handle = callbacks.register child_exited, "process-watch-#{@pid}", @ - C.g_child_watch_add ffi_cast('GPid', @pid), child_watch_callback, callbacks.cast_arg(@_exit_handle.id) + C.g_child_watch_add ffi_cast('GPid', @true_pid), child_watch_callback, callbacks.cast_arg(@_exit_handle.id) wait: => return if @exited @@ -127,7 +134,7 @@ class Process send_signal: (signal) => signal = signals[signal] if type(signal) == 'string' - C.kill(@pid, signal) + platform.send_signal @true_pid, signal pump: (on_stdout, on_stderr) => if on_stdout and not @stdout diff --git a/lib/howl/log.moon b/lib/howl/log.moon index a1d118e92..d5c5b0dfe 100644 --- a/lib/howl/log.moon +++ b/lib/howl/log.moon @@ -37,6 +37,7 @@ dispatch = (level, message) -> command_line\notify essentials, level if command_line.showing elseif not _G.howl.app.args.spec _G.print message + _G.io.flush! while #entries > config.max_log_entries and #entries > 0 table.remove entries, 1 diff --git a/lib/howl/sys.moon b/lib/howl/sys.moon index 00bb4099e..85219f9b9 100644 --- a/lib/howl/sys.moon +++ b/lib/howl/sys.moon @@ -2,6 +2,7 @@ -- License: MIT (see LICENSE.md at the top-level directory of the distribution) glib = require 'ljglibs.glib' +C = require('ffi').C env = setmetatable {}, { __index: (variable) => glib.getenv variable @@ -27,10 +28,61 @@ find_executable = (name) -> time = -> glib.get_real_time! / 1000000 +platform = {} + +platform.load_font_dir = (font_dir) -> + if jit.os == 'Windows' + require 'howl.cdefs.windows' + fonts = howl.io.File(font_dir).children + for font in *fonts + loaded = C.AddFontResourceExA font.path, C.fr_private, nil + if loaded == 0 + io.stderr\write "failed to load font #{font.path}\n" + io.stderr\flush! + else + require 'howl.cdefs.fontconfig' + C.FcConfigAppFontAddDir nil, font_dir + +platform.tmpname = -> + -- os.tmpname is broken on Windows and returns a filename prefixed with \. + -- This causes a lot of "Access denied"-related errors. + filename = assert os.tmpname! + -- Remove the prefix \. + filename = filename\sub 2 if jit.os == 'Windows' + filename + +platform.fd_to_stream = (win_type, unix_type, fd) -> + if jit.os == 'Windows' + win_type C._get_osfhandle fd + else + unix_type fd + +platform.default_shell = -> + if jit.os == 'Windows' + if howl.sys.env.MSYSCON + -- Running under MSYS2. + "#{env.WD}sh.exe" + else + "#{env.SYSTEMROOT}\\System32\\cmd.exe" + else + '/bin/sh' + +win_signals = {[tonumber C.sig_KILL]: true, [tonumber C.sig_INT]: true} +platform.send_signal = (pid, signal) -> + if jit.os == 'Windows' + error "Signal #{signal} is not supported on Windows" unless win_signals[signal] + -- On Bash, when a process exits due to a signal, it's exit code is + -- 128+{signal code}. Since killing a process like that doesn't + -- necessarily work on Windows, this emulates that exit code. + C.TerminateProcess(pid, 128+signal) + else + C.kill(pid, signal) + { :env, :find_executable :time, + :platform info: { os: jit.os\lower! } diff --git a/lib/howl/ui/content_box.moon b/lib/howl/ui/content_box.moon index 97213e92e..cdadefc9d 100644 --- a/lib/howl/ui/content_box.moon +++ b/lib/howl/ui/content_box.moon @@ -3,7 +3,7 @@ Gtk = require 'ljglibs.gtk' gobject_signal = require 'ljglibs.gobject.signal' -Background = require 'ljglibs.aux.background' +Background = require 'ljglibs.util.background' ffi = require 'ffi' {:signal} = howl diff --git a/lib/howl/ui/icons/font_awesome.moon b/lib/howl/ui/icons/font_awesome.moon index 9d464c71a..459725a3f 100644 --- a/lib/howl/ui/icons/font_awesome.moon +++ b/lib/howl/ui/icons/font_awesome.moon @@ -680,9 +680,15 @@ icon_text = { 'youtube-square': '\239\133\166', } +-- On Windows, when the font is loaded, it gets labeled as "FontAwesome". +family = if jit.os == 'Windows' + 'FontAwesome' +else + 'Font Awesome' + for name, text in pairs icon_text howl.ui.icon.define 'font-awesome-'..name, font: - family: 'Font Awesome' + :family size: 'small' :text diff --git a/lib/howl/ui/theme.moon b/lib/howl/ui/theme.moon index 29dfa2e4a..25379f5e3 100644 --- a/lib/howl/ui/theme.moon +++ b/lib/howl/ui/theme.moon @@ -205,7 +205,7 @@ with config .define name: 'font' description: 'The main font used within the application' - default: 'Liberation Mono, Monaco' + default: 'Liberation Mono, Monaco, Courier New' type_of: 'string' scope: 'global' diff --git a/lib/howl/ui/window.moon b/lib/howl/ui/window.moon index 3e8f284da..6e74d08f8 100644 --- a/lib/howl/ui/window.moon +++ b/lib/howl/ui/window.moon @@ -5,7 +5,7 @@ Gdk = require 'ljglibs.gdk' Gtk = require 'ljglibs.gtk' ffi = require 'ffi' gobject_signal = require 'ljglibs.gobject.signal' -Background = require 'ljglibs.aux.background' +Background = require 'ljglibs.util.background' import PropertyObject from howl.util.moon {:CommandLine, :Status, :theme} = howl.ui import signal from howl diff --git a/lib/howl/util/sandboxed_loader.moon b/lib/howl/util/sandboxed_loader.moon index bfab681b0..ac95ac853 100644 --- a/lib/howl/util/sandboxed_loader.moon +++ b/lib/howl/util/sandboxed_loader.moon @@ -24,7 +24,7 @@ new = (dir, name, sandbox_options = {}) -> "#{name}_file": (rel_path) -> dir / rel_path "#{name}_load": (rel_path, ...) -> - rel_path = rel_path\gsub '%.', File.separator + rel_path = rel_path\gsub '[./]', File.separator error 'Cyclic dependency in ' .. dir / rel_path if loading[rel_path] return loaded[rel_path] if loaded[rel_path] loading[rel_path] = true diff --git a/lib/ljglibs/cdefs/gio.moon b/lib/ljglibs/cdefs/gio.moon index d0ff423f7..8e5fbaaf4 100644 --- a/lib/ljglibs/cdefs/gio.moon +++ b/lib/ljglibs/cdefs/gio.moon @@ -111,6 +111,14 @@ ffi.cdef [[ typedef struct {} GUnixOutputStream; GUnixOutputStream * g_unix_output_stream_new (gint fd, gboolean close_fd); + /* GWin32InputStream */ + typedef struct {} GWin32InputStream; + GWin32InputStream * g_win32_input_stream_new (void* handle, gboolean close_handle); + + /* GWin32OutputStream */ + typedef struct {} GWin32OutputStream; + GWin32OutputStream * g_win32_output_stream_new (void* handle, gboolean close_handle); + /* GFile and friends */ typedef struct {} GFileInputStream; typedef struct {} GFileOutputStream; diff --git a/lib/ljglibs/cdefs/glib.moon b/lib/ljglibs/cdefs/glib.moon index 523f81da8..3dad29ab2 100644 --- a/lib/ljglibs/cdefs/glib.moon +++ b/lib/ljglibs/cdefs/glib.moon @@ -3,6 +3,11 @@ ffi = require 'ffi' +if ffi.os == 'Windows' + ffi.cdef 'typedef void* GPid;' +else + ffi.cdef 'typedef int GPid;' + ffi.cdef [[ typedef char gchar; typedef long glong; @@ -28,7 +33,6 @@ ffi.cdef [[ typedef double gdouble; typedef float gfloat; typedef const void * gconstpointer; - typedef int GPid; /* version definitions */ extern const guint glib_major_version; @@ -285,4 +289,6 @@ ffi.cdef [[ gint64 g_get_real_time (void); + void g_usleep(unsigned long ms); + ]] diff --git a/lib/ljglibs/cdefs/gobject.moon b/lib/ljglibs/cdefs/gobject.moon index 82732b28c..0fa15c2e4 100644 --- a/lib/ljglibs/cdefs/gobject.moon +++ b/lib/ljglibs/cdefs/gobject.moon @@ -27,7 +27,9 @@ ffi.cdef [[ /* GObject */ typedef struct {} GObject; - GObject g_object_new (GType object_type); + GObject g_object_new (GType object_type, + const gchar* first_property_name, + ...); gpointer g_object_ref (gpointer object); gpointer g_object_ref_sink (gpointer object); gboolean g_object_is_floating (gpointer object); diff --git a/lib/ljglibs/gio/win32input_stream.moon b/lib/ljglibs/gio/win32input_stream.moon new file mode 100644 index 000000000..eb022a230 --- /dev/null +++ b/lib/ljglibs/gio/win32input_stream.moon @@ -0,0 +1,21 @@ +-- Copyright 2014-2015 The Howl Developers +-- License: MIT (see LICENSE.md at the top-level directory of the distribution) + +require 'ljglibs.gio.input_stream' +ffi = require 'ffi' +core = require 'ljglibs.core' + +C = ffi.C + +release = (p) -> + C.g_input_stream_close, p, nil + C.g_object_unref(p) + +core.define 'GWin32InputStream < GInputStream', { + properties: { + handle: 'void*' + close_handle: 'gboolean' + } + +}, (t, handle, close_handle = true) -> + ffi.gc C.g_win32_input_stream_new(handle, close_handle), release diff --git a/lib/ljglibs/gio/win32output_stream.moon b/lib/ljglibs/gio/win32output_stream.moon new file mode 100644 index 000000000..a75f5d90e --- /dev/null +++ b/lib/ljglibs/gio/win32output_stream.moon @@ -0,0 +1,21 @@ +-- Copyright 2016 The Howl Developers +-- License: MIT (see LICENSE.md at the top-level directory of the distribution) + +require 'ljglibs.gio.output_stream' +ffi = require 'ffi' +core = require 'ljglibs.core' + +C = ffi.C + +release = (p) -> + C.g_output_stream_close, p, nil + C.g_object_unref(p) + +core.define 'GWin32OutputStream < GOutputStream', { + properties: { + handle: 'void*' + close_handle: 'gboolean' + } + +}, (t, handle, close_handle = true) -> + ffi.gc C.g_win32_output_stream_new(handle, close_handle), release diff --git a/lib/ljglibs/glib/spawn.moon b/lib/ljglibs/glib/spawn.moon index b86e00a0c..d1ed09eec 100644 --- a/lib/ljglibs/glib/spawn.moon +++ b/lib/ljglibs/glib/spawn.moon @@ -34,26 +34,57 @@ spawn = { argv = char_p_arr opts.argv pid = ffi_new 'GPid[1]' spawn_flags = core.parse_flags('G_SPAWN_', opts.flags) + if ffi.os == 'Windows' + spawn_flags = bit.bor spawn_flags, flags['DO_NOT_REAP_CHILD'] envp = get_envp opts.env stdin = opts.write_stdin and ffi_new('gint[1]') or nil stdout = opts.read_stdout and ffi_new('gint[1]') or nil stderr = opts.read_stderr and ffi_new('gint[1]') or nil - catch_error C.g_spawn_async_with_pipes, - opts.working_directory, - argv, envp, spawn_flags, - nil, nil, - pid, - stdin, stdout, stderr + -- XXX: This is a nasty hack!! + -- On Windows, for reasons unknown, g_spawn_async_with_pipes will randomly + -- fail with an EOF error (?). In order to work around that, on Windows, + -- the spawn will be attempted three times first. - pid = tonumber pid[0] + limit = if jit.os == 'Windows' + 3 + else + 1 + + for i=1,limit + status, err = pcall catch_error, C.g_spawn_async_with_pipes, + opts.working_directory, + argv, envp, spawn_flags, + nil, nil, + pid, + stdin, stdout, stderr + + if status + break + if jit.os == 'Windows' and err\match 'Failed to read from child pipe %(EOF%)' + _G.print i, limit + _G.io.flush! + if i < limit + -- Try sleeping for 1/10th second. + C.g_usleep 100000 + continue + error err + + true_pid = pid[0] + local pid + if ffi.os == 'Windows' + pid = C.GetProcessId true_pid + else + true_pid = tonumber true_pid + pid = true_pid caller_will_reap = bit.band(flags['DO_NOT_REAP_CHILD'], spawn_flags) != 0 destructor = caller_will_reap and nil or ffi_gc ffi_new('struct {}'), -> - C.g_spawn_close_pid pid + C.g_spawn_close_pid true_pid { + :true_pid :pid flags: spawn_flags stdin_pipe: stdin and stdin[0] or nil diff --git a/lib/ljglibs/gobject/object.moon b/lib/ljglibs/gobject/object.moon index 5e457ad28..6174be5d4 100644 --- a/lib/ljglibs/gobject/object.moon +++ b/lib/ljglibs/gobject/object.moon @@ -7,9 +7,9 @@ C, ffi_cast, ffi_new = ffi.C, ffi.cast, ffi.new lua_value = types.lua_value core.define 'GObject', { - new: (type) -> + new: (type, ...) -> error 'Undefined gtype passed in', 2 if type == 0 or type == nil - C.g_object_new type + C.g_object_new type, ..., nil ref: (o) -> return nil if o == nil @@ -42,5 +42,5 @@ core.define 'GObject', { set_typed: (k, type, v) => C.g_object_set @, k, ffi_cast(type, v), nil -}, (spec, type) -> spec.new type +}, (spec, type, ...) -> spec.new type, ... diff --git a/lib/ljglibs/spec/gio/file_spec.moon b/lib/ljglibs/spec/gio/file_spec.moon index 6c41719ee..7d265a28e 100644 --- a/lib/ljglibs/spec/gio/file_spec.moon +++ b/lib/ljglibs/spec/gio/file_spec.moon @@ -1,10 +1,22 @@ glib = require 'ljglibs.glib' GFile = require 'ljglibs.gio.file' +sep = if jit.os == 'Windows' + '\\' +else + '/' +ls = "#{sep}bin#{sep}ls" +demo_file = if jit.os == 'Windows' + "#{os.getenv 'WD'}ls.exe" +else + '/bin/ls' + describe 'GFile', -> with_tmpfile = (contents, f) -> p = os.tmpname! + if jit.os == 'Windows' + p = p\sub 2 fh = io.open p, 'wb' fh\write contents fh\close! @@ -15,28 +27,28 @@ describe 'GFile', -> if glib.check_version 2, 36, 0 describe 'new_for_commandline_arg_and_cwd(path, cwd)', -> it 'resolves a relative from ', -> - assert.equals '/bin/ls', GFile.new_for_commandline_arg_and_cwd('ls', '/bin').path + assert.equals ls, GFile.new_for_commandline_arg_and_cwd('ls', '/bin').path it 'resolves an absolute as is', -> - assert.equals '/bin/touch', GFile.new_for_commandline_arg_and_cwd('/bin/touch', '/home').path + assert.equals ls, GFile.new_for_commandline_arg_and_cwd('/bin/ls', '/home').path it '.path contains the path', -> - assert.equals '/bin/ls', GFile('/bin/ls').path + assert.equals ls, GFile('/bin/ls').path it '.uri contains an URI representing the path', -> assert.equal 'file:///foo.txt', GFile('/foo.txt').uri it '.exists returns true if the path exists', -> - assert.is_true GFile('/bin/ls').exists + assert.is_true GFile(demo_file).exists assert.is_false GFile('/pleasedontputadirectorylikethisinyourroot').exists it '.parent return the parent of the file', -> - assert.equal '/bin', GFile('/bin/ls').parent.path + assert.equal "#{sep}bin", GFile('/bin/ls').parent.path describe 'get_child(name)', -> it 'returns a new file for the given child', -> parent = GFile '/bin' - assert.equals '/bin/ls', parent\get_child('ls').path + assert.equals ls, parent\get_child('ls').path describe 'has_parent([file])', -> it 'returns true if the file has a parent', -> @@ -61,7 +73,7 @@ describe 'GFile', -> context 'for an existing file', -> it 'returns an info object', -> - f = GFile '/bin/ls' + f = GFile demo_file info = f\query_info '*', GFile.QUERY_INFO_NONE assert.is_false info.is_hidden assert.is_false info.is_symlink @@ -78,4 +90,4 @@ describe 'GFile', -> it 'tostring returns the path as a string', -> collectgarbage! file = GFile '/bin/ls' - assert.equal '/bin/ls', tostring file + assert.equal ls, tostring file diff --git a/lib/ljglibs/spec/gio/input_stream_spec.moon b/lib/ljglibs/spec/gio/input_stream_spec.moon index 277e4adfb..79af3fd54 100644 --- a/lib/ljglibs/spec/gio/input_stream_spec.moon +++ b/lib/ljglibs/spec/gio/input_stream_spec.moon @@ -6,6 +6,8 @@ glib = require 'ljglibs.glib' with_tmpfile = (contents, f) -> p = os.tmpname! + if jit.os == 'Windows' + p = p\sub 2 fh = io.open p, 'wb' fh\write contents fh\close! diff --git a/lib/ljglibs/spec/gio/output_stream_spec.moon b/lib/ljglibs/spec/gio/output_stream_spec.moon index c0933563b..a24ff74b2 100644 --- a/lib/ljglibs/spec/gio/output_stream_spec.moon +++ b/lib/ljglibs/spec/gio/output_stream_spec.moon @@ -5,6 +5,8 @@ with_tmpfile = (f) -> p = os.tmpname! + if jit.os == 'Windows' + p = p\sub 2 status, err = pcall f, p os.remove p error err unless status diff --git a/lib/ljglibs/spec/gio/subprocess_spec.moon b/lib/ljglibs/spec/gio/subprocess_spec.moon index 1e4f2a321..bf7a0dc82 100644 --- a/lib/ljglibs/spec/gio/subprocess_spec.moon +++ b/lib/ljglibs/spec/gio/subprocess_spec.moon @@ -19,7 +19,11 @@ describe 'Subprocess', -> describe 'creation', -> it 'raises an error for an unknown command', -> - assert.raises 'howlblargh', -> Subprocess nil, 'howlblargh', 'urk' + errstring = if jit.os == 'Windows' + 'No such file or directory' + else + 'howlblargh' + assert.raises errstring, -> Subprocess nil, 'howlblargh', 'urk' it 'returns a Subprocess for a valid command', -> assert.not_nil Subprocess(Subprocess.FLAGS_STDOUT_SILENCE, 'id') @@ -51,30 +55,32 @@ describe 'Subprocess', -> assert.equal 0, run('id').exit_status assert.not_equal 0, run('false').exit_status - context 'signal handling', -> - describe 'send_signal(signal) and .if_signaled', -> - it 'sends the specified signal to the process', -> + if jit.os != 'Windows' + context 'signal handling', -> + describe 'send_signal(signal) and .if_signaled', -> + it 'sends the specified signal to the process', -> + process = Subprocess(Subprocess.FLAGS_STDIN_PIPE, 'cat') + process\send_signal 9 + process\wait! + assert.is_true process.if_signaled + + it '.if_signaled returns false for a non-signaled process', -> + assert.is_false run('id').if_signaled + + it '.term_sig holds the signal used for terminating the process', -> + process = Subprocess(Subprocess.FLAGS_STDIN_PIPE, 'cat') + process\send_signal 9 + process\wait! + assert.equal 9, process.term_sig + + if jit.os != 'Windows' + describe 'force_exit()', -> + it 'tries to terminate the process in some way', -> process = Subprocess(Subprocess.FLAGS_STDIN_PIPE, 'cat') - process\send_signal 9 + process\force_exit! process\wait! assert.is_true process.if_signaled - it '.if_signaled returns false for a non-signaled process', -> - assert.is_false run('id').if_signaled - - it '.term_sig holds the signal used for terminating the process', -> - process = Subprocess(Subprocess.FLAGS_STDIN_PIPE, 'cat') - process\send_signal 9 - process\wait! - assert.equal 9, process.term_sig - - describe 'force_exit()', -> - it 'tries to terminate the process in some way', -> - process = Subprocess(Subprocess.FLAGS_STDIN_PIPE, 'cat') - process\force_exit! - process\wait! - assert.is_true process.if_signaled - describe 'wait_check()', -> it 'waits until the process is finished and returns true for a succesful termination', -> process = Subprocess(Subprocess.FLAGS_STDOUT_SILENCE, 'id') @@ -83,10 +89,11 @@ describe 'Subprocess', -> process = Subprocess(Subprocess.FLAGS_STDOUT_SILENCE, 'false') assert.raises 'exited', -> process\wait_check! - it 'raises an error if the process was killed by a signal', -> - process = Subprocess(Subprocess.FLAGS_STDIN_PIPE, 'cat') - process\send_signal 9 - assert.raises 'killed', -> process\wait_check! + if jit.os != 'Windows' + it 'raises an error if the process was killed by a signal', -> + process = Subprocess(Subprocess.FLAGS_STDIN_PIPE, 'cat') + process\send_signal 9 + assert.raises 'killed', -> process\wait_check! describe '.stdout_pipe', -> it 'allows reading process output', -> diff --git a/lib/ljglibs/spec/glib/spawn_spec.moon b/lib/ljglibs/spec/glib/spawn_spec.moon index b8062a158..cacf1e8ea 100644 --- a/lib/ljglibs/spec/glib/spawn_spec.moon +++ b/lib/ljglibs/spec/glib/spawn_spec.moon @@ -1,5 +1,12 @@ {:spawn} = require 'ljglibs.glib' -{:UnixInputStream, :UnixOutputStream} = require 'ljglibs.gio' +{:UnixInputStream, :UnixOutputStream, :Win32InputStream, :Win32OutputStream} = require 'ljglibs.gio' +ffi = require 'ffi' + +InputStream, OutputStream, shell = if jit.os == 'Windows' + forward = (cls) -> (pipe) -> cls ffi.C._get_osfhandle pipe + forward(Win32InputStream), forward(Win32OutputStream), "#{os.getenv 'WD'}/sh.exe" +else + UnixInputStream, UnixOutputStream, 'sh' describe 'spawn', -> @@ -24,7 +31,7 @@ describe 'spawn', -> flags: { 'SEARCH_PATH' } } assert.is_not_nil p.stdout_pipe - stdout = UnixInputStream p.stdout_pipe + stdout = InputStream p.stdout_pipe assert.equals 'foo\n', stdout\read_all! context '(when opts.write_stdin is given)', -> @@ -36,10 +43,10 @@ describe 'spawn', -> flags: { 'SEARCH_PATH' } } assert.is_not_nil p.stdin_pipe - stdin = UnixOutputStream p.stdin_pipe + stdin = OutputStream p.stdin_pipe stdin\write_all 'give it back!' stdin\close! - input_stream = UnixInputStream p.stdout_pipe + input_stream = InputStream p.stdout_pipe assert.equals 'give it back!', input_stream\read_all! context '(when opts.read_stderr is given)', -> @@ -50,27 +57,31 @@ describe 'spawn', -> flags: { 'SEARCH_PATH', 'STDOUT_TO_DEV_NULL' } } assert.is_not_nil p.stderr_pipe - stderr = UnixInputStream p.stderr_pipe + stderr = InputStream p.stderr_pipe assert.equals 'foo\n', stderr\read_all! context 'when .env is set', -> it 'spawns the process with the set values as the environment', -> p = spawn.async_with_pipes { - argv: { 'env' }, + argv: { shell, '-c', 'echo $MY_SOLE_VAR' }, read_stdout: true, flags: { 'SEARCH_PATH' }, env: { MY_SOLE_VAR: 'alone' } } - stdout = UnixInputStream p.stdout_pipe - assert.equals 'MY_SOLE_VAR=alone\n', stdout\read_all! + stdout = InputStream p.stdout_pipe + assert.equals 'alone\n', stdout\read_all! context 'when .working_dir is set', -> it 'spawns the process with the value as the working directory', -> + working_directory, expected_directory = if jit.os == 'Windows' + 'C:\\', '/c' + else + '/etc', '/etc' p = spawn.async_with_pipes { argv: { 'pwd' }, read_stdout: true, flags: { 'SEARCH_PATH' }, - working_directory: '/etc' + :working_directory } - stdout = UnixInputStream p.stdout_pipe - assert.equals '/etc\n', stdout\read_all! + stdout = InputStream p.stdout_pipe + assert.equals "#{expected_directory}\n", stdout\read_all! diff --git a/lib/ljglibs/spec/gobject/object_spec.moon b/lib/ljglibs/spec/gobject/object_spec.moon index 390bbf60c..f7cb0e0db 100644 --- a/lib/ljglibs/spec/gobject/object_spec.moon +++ b/lib/ljglibs/spec/gobject/object_spec.moon @@ -9,12 +9,12 @@ describe 'Object', -> context '(constructing)', -> it 'can be created using an existing gtype', -> type = Type.from_name 'GtkEventBox' - o = Object type + o = Object type, nil assert.is_not_nil o it 'raises an error if type is nil', -> type = Type.from_name 'GtkButton2' - assert.raises 'undefined', -> Object type + assert.raises 'undefined', -> Object type, nil describe 'get_typed(k, type)', -> it 'returns a property value converted according to ', -> diff --git a/lib/ljglibs/aux/background.moon b/lib/ljglibs/util/background.moon similarity index 100% rename from lib/ljglibs/aux/background.moon rename to lib/ljglibs/util/background.moon diff --git a/lint_config.moon b/lint_config.moon index 42d52807e..42fa9ba67 100644 --- a/lint_config.moon +++ b/lint_config.moon @@ -192,6 +192,7 @@ 'howl_async', 'howl_main_ctx' 'it', + 'make_hidden', 'moon', 'pump_mainloop', 'set_howl_loop', diff --git a/site/source/getit.md b/site/source/getit.md index 714783e44..a736a5fcc 100644 --- a/site/source/getit.md +++ b/site/source/getit.md @@ -5,9 +5,9 @@ title: Installation # Installing Howl Howl is developed on Linux, but it builds on other \*NIX platforms as well such -as FreeBSD and OpenBSD (with other \*BSDs presumably requiring only little -work). It should be possible to port to OSX or Windows, should any brave soul be -willing to put in the work. +as FreeBSD and OpenBSD (with other \*BSDs presumably requiring only little work), along with +Windows. It should be possible to port to OSX, should any brave soul be willing to +put in the work. You can install Howl by building it from source, either from a release or by cloning the repository from Github. @@ -26,6 +26,10 @@ _SHA1_: 1e48e4b2e50b6e587007a3de641349b57f4fbd1e __Release notes:__ [Howl 0.5 Released](/blog/2017/06/30/howl-0-5-released.html) +## Building or installing Howl on Windows + +See [the Windows-specific documentation](/getit.windows.html). + ## Building Howl from source ### Build requirements diff --git a/site/source/getit.windows.md b/site/source/getit.windows.md new file mode 100644 index 000000000..76c97fec1 --- /dev/null +++ b/site/source/getit.windows.md @@ -0,0 +1,65 @@ +--- +title: Building and installing Howl on Windows +--- + +# Using the binary distributions + +*TODO* + +# Building from source + + +## Setting up the build environment + +First of all, you need [MSYS2](http://www.msys2.org/). Once that's been +installed, open up the MSYS2 shell and run: + +``` +pacman -S make tar git wget patch +pacman -S mingw32/mingw-w64-i686-gcc mingw32/mingw-w64-i686-pkg-config +pacman -S mingw32/mingw-w64-i686-gtk3 mingw32/mingw-w64-i686-imagemagick +``` + +You can omit ImageMagick if desired, but then the Howl binary won't have the +icon files embedded inside. + +## Building + +**Open the MinGW32 shell. If you stay in the normal MSYS Shell, the build +will fail with obscure, downright weird errors.** If you accidentally began +building in the MSYS Shell, make sure you run `make clean` before continuing! + +Run: + +``` +make MAKE_RC=1 +``` + +to build with the icons embedded or plain: + +``` +make +``` + +to build without them. + +## Using Howl outside MSYS2 + +The binary file that's built won't be usable outside MSYS2. To make a version +usable within the rest of Windows, run: + +``` +make windist +``` + +This will a `windist` directory, containing: + +- A copy of Howl with the proper DLLs in place. +- A file `howl.zip`, suitable for portable usage. + +If you want an installer, make sure Inno Setup is installed and in your PATH, +and run: + +``` +make wininst +``` diff --git a/spec/buffer_spec.moon b/spec/buffer_spec.moon index 4a7c150f6..b59a65f35 100644 --- a/spec/buffer_spec.moon +++ b/spec/buffer_spec.moon @@ -1,6 +1,7 @@ import Buffer, config from howl import File from howl.io import with_tmpfile from File +ffi = require 'ffi' describe 'Buffer', -> buffer = (text) -> @@ -44,11 +45,13 @@ describe 'Buffer', -> it 'is updated whenever the buffer is changed in some way', -> b = buffer 'time' cur = b.last_changed + ffi.C.g_usleep 2000 b\insert 'foo', 1 assert.is_true b.last_changed > cur cur = b.last_changed + ffi.C.g_usleep 2000 b\delete 1, 3 assert.is_true b.last_changed > cur diff --git a/spec/bundle_spec.moon b/spec/bundle_spec.moon index d03ce0b37..086ebc55a 100644 --- a/spec/bundle_spec.moon +++ b/spec/bundle_spec.moon @@ -178,6 +178,7 @@ describe 'bundle', -> bundle.dirs = {dir} b_dir = dir / '.hidden' b_dir\mkdir! + make_hidden b_dir.path b_dir\join('init.lua').contents = bundle_init name: 'hidden' bundle.load_all! diff --git a/spec/interactions/buffer_selection_spec.moon b/spec/interactions/buffer_selection_spec.moon index 2641f4fcd..42bcde32d 100644 --- a/spec/interactions/buffer_selection_spec.moon +++ b/spec/interactions/buffer_selection_spec.moon @@ -114,4 +114,6 @@ describe 'buffer_selection', -> local buflist within_activity (-> interact.select_buffer :editor), -> buflist = get_ui_list_widget_column 2 - assert.same {'file1 [project1]', 'file1 [project2]', 'path1/file2', 'path2/file2'}, buflist + assert.same { 'file1 [project1]', 'file1 [project2]', + "path1#{File.separator}file2", + "path2#{File.separator}file2" }, buflist diff --git a/spec/interactions/file_selection_spec.moon b/spec/interactions/file_selection_spec.moon index 7e3485b61..d7de9640e 100644 --- a/spec/interactions/file_selection_spec.moon +++ b/spec/interactions/file_selection_spec.moon @@ -6,6 +6,7 @@ import File from howl.io import Window from howl.ui require 'howl.ui.icons.font_awesome' require 'howl.interactions.file_selection' +pathsep = File.separator describe 'file_selection', -> local tmpdir, command_line @@ -38,7 +39,7 @@ describe 'file_selection', -> local prompt within_activity interact.select_file, -> prompt = command_line.prompt - assert.same '~/', prompt + assert.same "~#{pathsep}", prompt context 'when a buffer associated with a file is open', -> it 'opens the directory of the current buffer, if any', -> @@ -46,37 +47,37 @@ describe 'file_selection', -> local prompt within_activity interact.select_file, -> prompt = command_line.prompt - assert.same tostring(tmpdir)..'/', prompt + assert.same tostring(tmpdir)..pathsep, File.expand_path prompt it 'typing a path opens the closest parent', -> prompts = {} within_activity interact.select_file, -> command_line\write tostring(tmpdir) - table.insert prompts, command_line.prompt - assert.same {tostring(tmpdir.parent) .. '/'}, prompts + table.insert prompts, File.expand_path command_line.prompt + assert.same {tostring(tmpdir.parent) .. pathsep}, prompts it 'typing "/" after a directory name opens the directory', -> local prompt within_activity interact.select_file, -> command_line\write tostring(tmpdir) .. '/' prompt = command_line.prompt - assert.same tostring(tmpdir) .. '/', prompt + assert.same tostring(tmpdir) .. pathsep, File.expand_path prompt it 'typing "../" switches to the parent of the current directory', -> prompts = {} within_activity interact.select_file, -> command_line\write tostring(tmpdir) .. '/' - table.insert prompts, command_line.prompt - command_line\write tostring(tmpdir) .. '../' - table.insert prompts, command_line.prompt - assert.same {tostring(tmpdir) .. '/', tostring(tmpdir.parent) .. '/'}, prompts + table.insert prompts, File.expand_path command_line.prompt + command_line\write tostring(tmpdir) .. '/../' + table.insert prompts, File.expand_path command_line.prompt + assert.same {tostring(tmpdir) .. pathsep, tostring(tmpdir.parent) .. pathsep}, prompts it 'typing "/" without any preceeding text changes to home directory', -> local prompt within_activity interact.select_file, -> command_line\write '/' prompt = command_line.prompt - assert.same '/', prompt + assert.same pathsep, prompt it 'shows files matching entered text in the current directory', -> files = { 'ab1', 'ab2', 'bc1' } @@ -103,7 +104,7 @@ describe 'file_selection', -> within_activity interact.select_file, -> prompt = command_line.prompt text = command_line.text - assert.same '~/', prompt + assert.same "~#{pathsep}", prompt assert.same 'matchthis', text context 'when spillover is an absolute path', -> @@ -113,7 +114,7 @@ describe 'file_selection', -> within_activity interact.select_file, -> prompt = command_line.prompt text = command_line.text - assert.same tostring(tmpdir)..'/', prompt + assert.same tostring(tmpdir)..pathsep, File.expand_path prompt assert.same 'matchthis', text context 'when spillover is a directory path that exists', -> @@ -126,7 +127,7 @@ describe 'file_selection', -> within_activity interact.select_file, -> prompt = command_line.prompt text = command_line.text - assert.same tostring(tmpdir / 'subdir')..'/', prompt + assert.same tostring(tmpdir / 'subdir')..pathsep, File.expand_path prompt assert.same '', text it 'opens the parent when specified without any trailing "/"', -> @@ -135,7 +136,7 @@ describe 'file_selection', -> within_activity interact.select_file, -> prompt = command_line.prompt text = command_line.text - assert.same tostring(tmpdir)..'/', prompt + assert.same tostring(tmpdir)..pathsep, File.expand_path prompt assert.same 'subdir', text context 'when config.hidden_file_extensions is set', -> @@ -168,10 +169,11 @@ describe 'file_selection', -> context 'in subtree mode', -> it 'shows files and directories in the subtree', -> - files = { 'ab1', 'ab2/', 'ab2/xy', 'ef/', 'ef/gh/', 'ef/gh/ab4'} + files = { 'ab1', "ab2#{pathsep}", "ab2#{pathsep}xy", "ef#{pathsep}", + "ef#{pathsep}gh#{pathsep}", "ef#{pathsep}gh#{pathsep}ab4" } for name in *files f = tmpdir / name - if name\ends_with '/' + if name\ends_with pathsep f\mkdir! else f.contents = 'a' @@ -185,7 +187,9 @@ describe 'file_selection', -> items2 = get_ui_list_widget_column(2) assert.same files, items - assert.same {'ab1', 'ab2/', 'ab2/xy', 'ef/gh/ab4'}, items2 + expected = {'ab1', "ab2#{pathsep}", "ab2#{pathsep}xy", + "ef#{pathsep}gh#{pathsep}ab4"} + assert.same expected, items2 describe 'interact.select_directory', -> @@ -205,4 +209,4 @@ describe 'file_selection', -> command_line\write tostring(tmpdir) .. '/' items = get_ui_list_widget_column(2) - assert.same { './', 'dir1/', 'dir2/' }, items + assert.same { ".#{pathsep}", "dir1#{pathsep}", "dir2#{pathsep}" }, items diff --git a/spec/io/file_spec.moon b/spec/io/file_spec.moon index a2f0cb014..78a5e8295 100644 --- a/spec/io/file_spec.moon +++ b/spec/io/file_spec.moon @@ -1,5 +1,7 @@ import File from howl.io +pathsep = File.separator + describe 'File', -> describe 'tmpfile()', -> @@ -34,33 +36,34 @@ describe 'File', -> describe 'expand_path(path)', -> it 'expands "~" into the full path of the home directory', -> - assert.equals "#{os.getenv('HOME')}/foo.txt", (File.expand_path '~/foo.txt') + assert.equals "#{os.getenv('HOME')}#{pathsep}foo.txt", + File.expand_path("~#{pathsep}foo.txt") describe 'new(p, cwd)', -> it 'accepts a string as denothing a path', -> - File '/bin/ls' + File "#{pathsep}bin#{pathsep}ls" it 'accepts other files as well', -> - f = File '/bin/ls' + f = File "#{pathsep}bin#{pathsep}ls" f2 = File f assert.equal f, f2 context 'when is specified', -> it 'resolves a string

relative to ', -> - assert.equal '/bin/ls', File('ls', '/bin').path + assert.equal "#{pathsep}bin#{pathsep}ls", File('ls', '/bin').path it 'resolves an absolute string

as the absolute path', -> - assert.equal '/bin/ls', File('/bin/ls', '/home').path + assert.equal "#{pathsep}bin#{pathsep}ls", File("#{pathsep}bin#{pathsep}ls", '/home').path it 'accepts other Files as ', -> - assert.equal '/bin/ls', File('ls', File('/bin')).path + assert.equal "#{pathsep}bin#{pathsep}ls", File('ls', File('/bin')).path describe '.is_absolute', -> it 'returns true if the given path is absolute', -> assert.is_true File.is_absolute '/bin/ls' assert.is_true File.is_absolute 'c:\\\\bin\\ls' - it 'returns false if the given path is absolute', -> + it 'returns false if the given path is not absolute', -> assert.is_false File.is_absolute 'bin/ls' assert.is_false File.is_absolute 'bin\\ls' @@ -73,14 +76,21 @@ describe 'File', -> assert.equal 'base.ext', File('/foo/base.ext').display_name it 'has a trailing separator for directories', -> - assert.equal 'bin/', File('/usr/bin').display_name + local dir, expected + if jit.os == 'Windows' + dir = File os.getenv 'SYSTEMROOT' + expected = "#{dir.basename}\\" + else + dir = File '/usr/bin' + expected = 'bin/' + assert.equal expected, dir.display_name it '.extension returns the extension of the path', -> assert.equal File('/foo/base.ext').extension, 'ext' assert.equal File('/foo/base.ex+').extension, 'ex+' it '.path returns the path of the file', -> - assert.equal '/foo/base.ext', File('/foo/base.ext').path + assert.equal "#{pathsep}foo#{pathsep}base.ext", File('/foo/base.ext').path it '.uri returns an URI representing the path', -> assert.equal File('/foo.txt').uri, 'file:///foo.txt' @@ -91,7 +101,7 @@ describe 'File', -> describe '.short_path', -> it 'returns the path with the home directory replace by "~"', -> file = File(os.getenv('HOME')) / 'foo.txt' - assert.equal '~/foo.txt', file.short_path + assert.equal "~#{pathsep}foo.txt", file.short_path describe 'contents', -> it 'assigning a string writes the string to the file', -> @@ -110,7 +120,7 @@ describe 'File', -> assert.equal file.contents, 'hello world' it '.parent return the parent of the file', -> - assert.equal File('/bin/ls').parent.path, '/bin' + assert.equal File('/bin/ls').parent.path, "#{pathsep}bin" it '.children returns a table of children', -> with_tmpdir (dir) -> @@ -121,9 +131,14 @@ describe 'File', -> assert.same [v.basename for v in *kids], { 'child1', 'child2' } it '.file_type is a string describing the file type', -> - assert.equal 'directory', File('/bin').file_type - assert.equal 'regular', File('/bin/ls').file_type - assert.equal 'special', File('/dev/null').file_type + if jit.os == 'Windows' + sysroot = os.getenv 'SYSTEMROOT' + assert.equal 'directory', File(sysroot).file_type + assert.equal 'regular', File("#{sysroot}#{pathsep}explorer.exe").file_type + else + assert.equal 'directory', File('/bin').file_type + assert.equal 'regular', File('/bin/ls').file_type + assert.equal 'special', File('/dev/null').file_type it '.writeable is true if the file represents a entry that can be written to', -> with_tmpdir (dir) -> @@ -202,16 +217,16 @@ describe 'File', -> assert.same { 'first', ' line' }, { file\read 5, '*l' } it 'join() returns a new file representing the specified child', -> - assert.equal File('/bin')\join('ls').path, '/bin/ls' + assert.equal File('/bin')\join('ls').path, "#{pathsep}bin#{pathsep}ls" it 'relative_to_parent() returns a path relative to the specified parent', -> parent = File '/bin' - file = File '/bin/ls' + file = File "#{pathsep}bin#{pathsep}ls" assert.equal 'ls', file\relative_to_parent(parent) it 'is_below(dir) returns true if the file is located beneath

', -> parent = File '/bin' - assert.is_true File('/bin/ls')\is_below parent + assert.is_true File("#{pathsep}bin#{pathsep}ls")\is_below parent assert.is_true File('/bin/sub/ls')\is_below parent assert.is_false File('/usr/bin/ls')\is_below parent @@ -313,10 +328,10 @@ describe 'File', -> normalized = [f\relative_to_parent dir for f in *files] assert.same { 'child1', - 'child1/sandwich.lua', - 'child1/sub_child.txt', - 'child1/sub_dir', - 'child1/sub_dir/deep.lua', + "child1#{pathsep}sandwich.lua", + "child1#{pathsep}sub_child.txt", + "child1#{pathsep}sub_dir", + "child1#{pathsep}sub_dir#{pathsep}deep.lua", 'child2' }, normalized @@ -328,10 +343,10 @@ describe 'File', -> assert.same normalized, { 'child2', 'child1', - 'child1/sandwich.lua', - 'child1/sub_child.txt', - 'child1/sub_dir', - 'child1/sub_dir/deep.lua', + "child1#{pathsep}sandwich.lua", + "child1#{pathsep}sub_child.txt", + "child1#{pathsep}sub_dir", + "child1#{pathsep}sub_dir#{pathsep}deep.lua", } context 'when filter: is passed as an option', -> @@ -345,13 +360,13 @@ describe 'File', -> describe 'meta methods', -> it '/ and .. joins the file with the specified argument', -> file = File('/bin') - assert.equal (file / 'ls').path, '/bin/ls' - assert.equal (file .. 'ls').path, '/bin/ls' + assert.equal (file / 'ls').path, "#{pathsep}bin#{pathsep}ls" + assert.equal (file .. 'ls').path, "#{pathsep}bin#{pathsep}ls" it 'tostring returns the result of File.tostring', -> - file = File '/bin/ls' + file = File "#{pathsep}bin#{pathsep}ls" assert.equal file\tostring!, tostring file it '== returns true if the files point to the same path', -> - assert.equal File('/bin/ls'), File('/bin/ls') + assert.equal File("#{pathsep}bin#{pathsep}ls"), File("#{pathsep}bin#{pathsep}ls") diff --git a/spec/io/process_spec.moon b/spec/io/process_spec.moon index 1325c59aa..49cd39a47 100644 --- a/spec/io/process_spec.moon +++ b/spec/io/process_spec.moon @@ -2,40 +2,69 @@ Process = howl.io.Process File = howl.io.File glib = require 'ljglibs.glib' +sh, echo = if jit.os == 'Windows' + "#{howl.sys.env.WD}sh.exe", "#{howl.sys.env.WD}echo.exe" +else + '/bin/sh', '/bin/echo' + +fix_paths = (path) -> + if jit.os == 'Windows' + -- On MSYS, often the shell will output paths like: + -- /usr/bin/sh + -- but the specs use (and therefore expect) stuff like: + -- C:/msys64/usr/bin/sh.exe + -- This fixes up those paths to make the latter like the former. + path\gsub('\\', '/')\gsub('.*msys[^/]*/', '/')\gsub('%.exe$', '') + else + path + describe 'Process', -> run = (...) -> with Process cmd: ... \wait! + procs = {} + collected_process = (...) -> + proc = Process ... + table.insert procs, proc + proc + collect = -> + for proc in *procs + proc\wait! + describe 'Process(opts)', -> it 'raises an error if opts.cmd is missing or invalid', -> assert.raises 'cmd', -> Process {} assert.raises 'cmd', -> Process cmd: 2 - assert.not_error -> Process cmd: 'id' - assert.not_error -> Process cmd: {'echo', 'foo'} + assert.not_error -> collected_process cmd: 'id' + assert.not_error -> collected_process cmd: {'echo', 'foo'} it 'returns a process object', -> - assert.equal 'Process', typeof Process cmd: 'true' + assert.equal 'Process', typeof collected_process cmd: 'true' it 'raises an error for an unknown command', -> - assert.raises 'howlblargh', -> Process cmd: {'howlblargh'} + errstring = if jit.os == 'Windows' + 'No such file or directory' + else + 'howlblargh' + assert.raises errstring, -> Process cmd: {'howlblargh'} it 'sets .argv to the parsed command line', -> - p = Process cmd: {'echo', 'foo'} + p = collected_process cmd: {'echo', 'foo'} assert.same {'echo', 'foo'}, p.argv - p = Process cmd: 'echo "foo bar"' - assert.same { '/bin/sh', '-c', 'echo "foo bar"'}, p.argv + p = collected_process cmd: 'echo "foo bar"' + assert.same { sh, '-c', 'echo "foo bar"'}, p.argv it 'allows specifying a different shell', -> - p = Process cmd: 'foo', shell: '/bin/echo' - assert.same { '/bin/echo', '-c', 'foo'}, p.argv + p = collected_process cmd: 'foo', shell: echo + assert.same { echo, '-c', 'foo'}, p.argv describe 'Process.execute(cmd, opts)', -> it 'executes the specified command and return ', (done) -> howl_async -> - out, err, p = Process.execute {'sh', '-c', 'cat; echo foo >&2'}, stdin: 'reverb' + out, err, p = Process.execute {sh, '-c', 'cat; echo foo >&2'}, stdin: 'reverb' assert.equal 'reverb', out assert.equal 'foo\n', err assert.equal 'Process', typeof(p) @@ -45,12 +74,13 @@ describe 'Process', -> howl_async -> status, out = pcall Process.execute, 'echo $0' assert.is_true status - assert.equal '/bin/sh\n', out + expected = fix_paths sh + assert.equal "#{expected}\n", out done! it "allows specifying a different shell", (done) -> howl_async -> - status, out, _, process = pcall Process.execute, 'blargh', shell: '/bin/echo' + status, out, _, process = pcall Process.execute, 'blargh', shell: echo assert.is_true status assert.match out, 'blargh' assert.equal 'blargh', process.command_line @@ -65,7 +95,11 @@ describe 'Process', -> it 'opts.env sets the process environment', (done) -> howl_async -> - out = Process.execute {'env'}, env: { foo: 'bar' } + cmd = if jit.os == 'Windows' + 'env' + else + {'env'} + out = Process.execute cmd, env: { foo: 'bar' } assert.equal 'foo=bar', out.stripped done! @@ -118,21 +152,24 @@ describe 'Process', -> done! context 'when handlers are not specified', -> - it 'collects and returns and output', -> - p = Process cmd: 'echo foo', read_stdout: true - stdout, stderr = p\pump! - assert.equals 'foo\n', stdout - assert.is_nil stderr - - p = Process cmd: 'echo err >&2', read_stderr: true - stdout, stderr = p\pump! - assert.equals 'err\n', stderr - assert.is_nil stdout - - p = Process cmd: 'echo out; echo err >&2', read_stdout: true, read_stderr: true - stdout, stderr = p\pump! - assert.equals 'out\n', stdout - assert.equals 'err\n', stderr + it 'collects and returns and output', (done) -> + howl_async -> + p = Process cmd: 'echo foo', read_stdout: true + stdout, stderr = p\pump! + assert.equals 'foo\n', stdout + assert.is_nil stderr + + p = Process cmd: 'echo err >&2', read_stderr: true + stdout, stderr = p\pump! + assert.equals 'err\n', stderr + assert.is_nil stdout + + p = Process cmd: 'echo out; echo err >&2', read_stdout: true, read_stderr: true + stdout, stderr = p\pump! + assert.equals 'out\n', stdout + assert.equals 'err\n', stderr + + done! describe 'wait()', -> it 'waits until the process is finished', (done) -> @@ -187,10 +224,13 @@ describe 'Process', -> done! describe '.exit_status', -> - it 'is nil for a running process', -> - p = Process cmd: { 'sh', '-c', "sleep 1; true" } - assert.is_nil p.exit_status - p\wait! + it 'is nil for a running process', (done) -> + settimeout 2 + howl_async -> + p = Process cmd: { 'sh', '-c', "sleep 1; true" } + assert.is_nil p.exit_status + p\wait! + done! it 'is nil for a signalled process', (done) -> howl_async -> @@ -215,18 +255,23 @@ describe 'Process', -> describe '.working_directory', -> context 'when provided during launch', -> + bindir = if jit.os == 'Windows' + howl.sys.env.SYSTEMROOT + else + '/bin' + it 'is the same directory', -> - cwd = File '/bin' - p = Process(cmd: 'true', working_directory: cwd) + cwd = File bindir + p = collected_process(cmd: 'true', working_directory: cwd) assert.equal cwd, p.working_directory it 'is always a File instance', -> - p = Process(cmd: 'true', working_directory: '/bin') - assert.equal 'File', typeof p.working_directory + p = collected_process(cmd: 'true', working_directory: bindir) + assert.equal 'File', typeof p.working_directory context 'when not provided', -> it 'is the current working directory', -> - p = Process(cmd: 'true') + p = collected_process(cmd: 'true') assert.equal File(glib.get_current_dir!), p.working_directory describe '.successful', -> @@ -251,7 +296,7 @@ describe 'Process', -> describe '.stdout', -> it 'allows reading process output', (done) -> howl_async -> - p = Process cmd: {'echo', 'one\ntwo'}, read_stdout: true + p = collected_process cmd: {'echo', 'one\ntwo'}, read_stdout: true assert.equals 'one\ntwo\n', p.stdout\read! assert.is_nil p.stdout\read! done! @@ -259,7 +304,7 @@ describe 'Process', -> describe '.stderr', -> it 'allows reading process error output', (done) -> howl_async -> - p = Process cmd: {'sh', '-c', 'echo foo >&2'}, read_stderr: true + p = collected_process cmd: {'sh', '-c', 'echo foo >&2'}, read_stderr: true assert.equals 'foo\n', p.stderr\read! done! @@ -306,6 +351,7 @@ describe 'Process', -> describe 'Process.running', -> it 'is a table of currently running processes, keyed by pid', (done) -> howl_async -> + collect! assert.same {}, Process.running p = Process cmd: {'cat'}, write_stdin: true assert.same {[p.pid]: p}, Process.running diff --git a/spec/project_spec.moon b/spec/project_spec.moon index 951dd8b6e..17cb8bb5f 100644 --- a/spec/project_spec.moon +++ b/spec/project_spec.moon @@ -1,5 +1,6 @@ import Project, VC from howl import File from howl.io +ffi = require 'ffi' describe 'Project', -> before_each -> @@ -100,4 +101,11 @@ describe 'Project', -> hidden\touch! backup = dir / 'config~' backup\touch! - assert.same { regular.path }, [f.path for f in *Project(dir)\files!] + + expected = { regular.path } + if ffi.os == 'Windows' + make_hidden hidden.path + -- glib on Windows has no notion of a "backup file" + table.insert expected, 1, backup.path + + assert.same expected, [f.path for f in *Project(dir)\files!] diff --git a/spec/support/spec_helper.moon b/spec/support/spec_helper.moon index b08442dea..6f4640d20 100644 --- a/spec/support/spec_helper.moon +++ b/spec/support/spec_helper.moon @@ -7,6 +7,9 @@ import theme from howl.ui import dispatch, signal, config from howl _G.Spy = require 'howl.spec.spy' +if jit.os == 'Windows' and not howl.sys.env.MSYSCON + error 'These specs must be run under MSYS2!' + -- additional aliases export context = describe @@ -92,7 +95,7 @@ howl_loop = setmetatable { export set_howl_loop = -> _G.setloop howl_loop export howl_async = (f) -> - _G.setloop howl_loop + set_howl_loop! co = coroutine.create busted.async(f) status, err = coroutine.resume co error err unless status @@ -132,3 +135,36 @@ export collect_memory = -> used = collectgarbage('count') break if used >= mem mem = used + +export assert_memory_stays_within = (units, iterations, f) -> + val, unit = units\match '(%d+)(%S+)' + if not (val and unit) and (unit == '%' or unit == 'Kb') + error "Unknown unit specifier '#{units}'" + + val = tonumber val + f! + collect_memory! + baseline = math.ceil(collectgarbage 'count') + total_used = 0 + + for i = 1, iterations + f! + collect_memory! + used = math.ceil(collectgarbage 'count') + total_used += used + + avg_used = total_used / iterations + diff = avg_used - baseline + percentual = (diff / baseline) * 100 + if diff > 0 + if (unit == '%' and percentual > val) or (unit == 'Kb' and diff > val) + err = string.format "Memory increased on average from %dKb -> %dKb (diff = %dKb, %.2f%%)", + baseline, avg_used, diff, percentual + error err + +if jit.os == 'Windows' + require 'howl.cdefs.windows' + FILE_ATTRIBUTE_HIDDEN = 2 + export make_hidden = (path) -> C.SetFileAttributesA(path, FILE_ATTRIBUTE_HIDDEN) +else + export make_hidden = (path) -> nil diff --git a/spec/ui/theme_spec.moon b/spec/ui/theme_spec.moon index f93e0fe34..9df0044db 100644 --- a/spec/ui/theme_spec.moon +++ b/spec/ui/theme_spec.moon @@ -2,9 +2,14 @@ import config, signal from howl import theme from howl.ui import File from howl.io +ffi = require 'ffi' serpent = require 'serpent' -font = name: 'Liberation Mono', size: 11, bold: true +font_name = if ffi.os == 'Windows' + 'Courier New' +else + 'Liberation Mono' +font = name: font_name, size: 11, bold: true spec_theme = { window: diff --git a/spec/util/paths_spec.moon b/spec/util/paths_spec.moon index b57206698..7b5860aa1 100644 --- a/spec/util/paths_spec.moon +++ b/spec/util/paths_spec.moon @@ -1,6 +1,8 @@ import File from howl.io import paths from howl.util +pathsep = File.separator + describe 'paths', -> local tmpdir @@ -17,19 +19,21 @@ describe 'paths', -> assert.same {File.home_dir, ''}, {paths.get_dir_and_leftover ''} it 'returns the root directory for "/"', -> - assert.same {File.home_dir.root_dir, ''}, {paths.get_dir_and_leftover '/'} + assert.same {File.home_dir.root_dir, ''}, + {paths.get_dir_and_leftover File.home_dir.root_dir.path} it 'returns the matched path and unmatched parts of a path', -> assert.same {tmpdir, 'unmatched'}, {paths.get_dir_and_leftover tostring(tmpdir / 'unmatched')} - it 'when given a directory path ending in "/", matches the given directory', -> - assert.same {tmpdir / 'subdir', ''}, {paths.get_dir_and_leftover tostring(tmpdir)..'/subdir/'} + it 'when given a directory path ending in the path separator, matches the given directory', -> + assert.same {tmpdir / 'subdir', ''}, {paths.get_dir_and_leftover tostring(tmpdir).."#{pathsep}subdir#{pathsep}"} it 'when given a directory path not ending in "/", matches the parent directory', -> - assert.same {tmpdir, 'subdir'}, {paths.get_dir_and_leftover tostring(tmpdir)..'/subdir'} + assert.same {tmpdir, 'subdir'}, {paths.get_dir_and_leftover tostring(tmpdir).."#{pathsep}subdir"} it 'unmatched part can contain slashes', -> - assert.same {tmpdir, 'unmatched/no/such/file'}, {paths.get_dir_and_leftover tostring(tmpdir / 'unmatched/no/such/file')} + assert.same {tmpdir, "unmatched#{pathsep}no#{pathsep}such#{pathsep}file"}, + {paths.get_dir_and_leftover tostring(tmpdir / 'unmatched/no/such/file')} context 'is given a non absolute path', -> it 'uses the home dir as the base path', -> @@ -43,4 +47,8 @@ describe 'paths', -> (tmpdir / file).contents = 'a' files = paths.subtree_reader tmpdir - assert.same {'a', 'a/x', 'b', 'b/y', 'b/c', 'b/c/z'}, [file\relative_to_parent(tmpdir) for file in *files] + expected = { + 'a', "a#{pathsep}x", 'b', "b#{pathsep}y", "b#{pathsep}c", + "b#{pathsep}c#{pathsep}z" + } + assert.same expected, [file\relative_to_parent(tmpdir) for file in *files] diff --git a/spec/util/sandboxed_loader_spec.moon b/spec/util/sandboxed_loader_spec.moon index 62b57a1f1..1da7c5643 100644 --- a/spec/util/sandboxed_loader_spec.moon +++ b/spec/util/sandboxed_loader_spec.moon @@ -20,13 +20,13 @@ describe 'SandboxedLoader', -> describe '_load(rel_basename)', -> it 'loads relative bytecode, lua and moonscript files', -> - dir\join('aux_lua.lua').contents = '_G.loaded_lua = true' - dir\join('aux_moon.moon').contents = '_G.loaded_moon = true' - dir\join('aux_bc.bc').contents = string.dump loadstring('_G.loaded_bc = true'), false + dir\join('util_lua.lua').contents = '_G.loaded_lua = true' + dir\join('util_moon.moon').contents = '_G.loaded_moon = true' + dir\join('util_bc.bc').contents = string.dump loadstring('_G.loaded_bc = true'), false loader -> - foo_load 'aux_lua' - foo_load 'aux_moon' - foo_load 'aux_bc' + foo_load 'util_lua' + foo_load 'util_moon' + foo_load 'util_bc' assert.is_true _G.loaded_lua assert.is_true _G.loaded_moon @@ -43,13 +43,13 @@ describe 'SandboxedLoader', -> assert.equal 'lua', loader -> foo_load 'two' it 'only loads each file once', -> - dir\join('aux.lua').contents = [[ + dir\join('util.lua').contents = [[ _G.load_count = _G.load_count or 0 _G.load_count = _G.load_count + 1 return _G.load_count ]] - assert.equals 1, loader -> foo_load 'aux' - assert.equals 1, loader -> foo_load 'aux' + assert.equals 1, loader -> foo_load 'util' + assert.equals 1, loader -> foo_load 'util' context '(loading files from sub directories)', -> local sub_dir @@ -79,10 +79,10 @@ describe 'SandboxedLoader', -> assert.equals 'lua', loader -> foo_load 'subdir' it 'signals an error upon cyclic dependencies', -> - dir\join('aux.lua').contents = 'foo_load("aux2")' - dir\join('aux2.lua').contents = 'foo_load("aux")' - assert.raises 'Cyclic dependency', -> loader -> foo_load 'aux' + dir\join('util.lua').contents = 'foo_load("util2")' + dir\join('util2.lua').contents = 'foo_load("util")' + assert.raises 'Cyclic dependency', -> loader -> foo_load 'util' it 'allows passing parameters to the loaded file', -> - dir\join('aux.lua').contents = 'return ...' - assert.equal 123, loader -> foo_load 'aux', 123 + dir\join('util.lua').contents = 'return ...' + assert.equal 123, loader -> foo_load 'util', 123 diff --git a/src/001-luajit-ffi-win32-walk-all-modules.patch b/src/001-luajit-ffi-win32-walk-all-modules.patch new file mode 100644 index 000000000..3d105bb30 --- /dev/null +++ b/src/001-luajit-ffi-win32-walk-all-modules.patch @@ -0,0 +1,102 @@ +--- LuaJIT-2.1.0-beta1-orig/src/lj_clib.c 2015-08-25 16:35:00.000000000 -0500 ++++ LuaJIT-2.1.0-beta1/src/lj_clib.c 2016-06-01 08:56:37.805150100 -0500 +@@ -146,6 +146,7 @@ + + #define WIN32_LEAN_AND_MEAN + #include ++#include + + #ifndef GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + #define GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 4 +@@ -231,33 +232,69 @@ + } + } + ++typedef struct LdrListEntry { ++ struct LdrListEntry *Flink; ++ struct LdrListEntry *Blink; ++} LdrListEntry; ++ ++typedef struct LdrDataTableEntry { ++ LdrListEntry InLoadOrderLinks; ++ LdrListEntry InMemoryOrderLinks; ++ LdrListEntry InInitializationOrderLinks; ++ void *ModuleBase; ++} LdrDataTableEntry; ++ ++typedef struct LdrData { ++ unsigned int Length; ++ int Initialized; ++ void *SsHandle; ++ LdrListEntry InLoadOrderModuleList; ++ LdrListEntry InMemoryOrderModuleList; ++ LdrListEntry InInitializationOrderModuleList; ++ void *EntryInProgress; ++} LdrData; ++ ++typedef struct Win32Peb { ++ void *Reserved[3]; ++ LdrData *Ldr; ++} Win32Peb; ++ ++/* These definitions depend on MingW's shims for the MSVC instrinsics. */ ++/* Can copy those shims from intrin.h if a non-mingw gcc is encountered. */ ++#if _WIN64 ++ ++static Win32Peb *clib_win32_getpeb() { ++ return (Win32Peb *)__readgsqword(0x60); ++} ++ ++#else ++ ++static Win32Peb *clib_win32_getpeb() { ++ return (Win32Peb *)__readfsdword(0x30); ++} ++ ++#endif ++ + static void *clib_getsym(CLibrary *cl, const char *name) + { + void *p = NULL; + if (cl->handle == CLIB_DEFHANDLE) { /* Search default libraries. */ +- MSize i; +- for (i = 0; i < CLIB_HANDLE_MAX; i++) { +- HINSTANCE h = (HINSTANCE)clib_def_handle[i]; +- if (!(void *)h) { /* Resolve default library handles (once). */ +- switch (i) { +- case CLIB_HANDLE_EXE: GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, NULL, &h); break; +- case CLIB_HANDLE_DLL: +- GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS|GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, +- (const char *)clib_def_handle, &h); +- break; +- case CLIB_HANDLE_CRT: +- GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS|GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, +- (const char *)&_fmode, &h); +- break; +- case CLIB_HANDLE_KERNEL32: h = LoadLibraryExA("kernel32.dll", NULL, 0); break; +- case CLIB_HANDLE_USER32: h = LoadLibraryExA("user32.dll", NULL, 0); break; +- case CLIB_HANDLE_GDI32: h = LoadLibraryExA("gdi32.dll", NULL, 0); break; +- } +- if (!h) continue; +- clib_def_handle[i] = (void *)h; ++ LdrListEntry *begin, *end; ++ LdrDataTableEntry *curr; ++ Win32Peb *peb = clib_win32_getpeb(); ++ end = &peb->Ldr->InLoadOrderModuleList; ++ begin = end->Flink; ++ while (begin != end) { ++ curr = (LdrDataTableEntry *)((char *)begin); ++ /* From what I've seen, this is unnecessary since ModuleBase == HMODULE */ ++ HINSTANCE h; ++ GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS|GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, ++ (const char *)curr->ModuleBase, &h); ++ if (h) { ++ p = (void *)GetProcAddress(h, name); ++ if (p) break; + } +- p = (void *)GetProcAddress(h, name); +- if (p) break; ++ begin = begin->Flink; + } + } else { + p = (void *)GetProcAddress((HINSTANCE)cl->handle, name); diff --git a/src/002-luajit-win32-build-static.patch b/src/002-luajit-win32-build-static.patch new file mode 100644 index 000000000..f83f6731b --- /dev/null +++ b/src/002-luajit-win32-build-static.patch @@ -0,0 +1,15 @@ +--- LuaJIT-2.1.0-beta1-orig/src/Makefile 2015-08-25 16:35:00.000000000 -0500 ++++ LuaJIT-2.1.0-beta1/src/Makefile 2016-05-31 18:50:20.426644400 -0500 +@@ -69,10 +69,10 @@ + # as dynamic mode. + # + # Mixed mode creates a static + dynamic library and a statically linked luajit. +-BUILDMODE= mixed ++#BUILDMODE= mixed + # + # Static mode creates a static library and a statically linked luajit. +-#BUILDMODE= static ++BUILDMODE= static + # + # Dynamic mode creates a dynamic library and a dynamically linked luajit. + # Note: this executable will only run when the library is installed! diff --git a/src/Makefile b/src/Makefile index e61985771..734117947 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,9 +1,23 @@ PREFIX ?= /usr/local UNAME_S := $(shell uname -s) +CWD := $(shell pwd) +ifneq (, $(findstring _NT-,$(UNAME_S))) + WIN32 = 1 +endif GTK = gtk+-3.0 GTK_CFLAGS = $(shell pkg-config --cflags $(GTK)) -GTK_LIBS = $(shell pkg-config --libs $(GTK) gmodule-2.0 gio-unix-2.0) +GTK_LIBNAMES = $(GTK) gmodule-2.0 gio-unix-2.0 +ifdef WIN32 + GTK_LIBNAMES = $(GTK) gmodule-2.0 +endif +GTK_LIBS = $(shell pkg-config --libs $(GTK_LIBNAMES)) + +ifdef WIN32 + HOWL_EXE=howl.exe +else + HOWL_EXE=howl +endif LUAJIT_VER = LuaJIT-2.1.0-beta3 LUAJIT_CHECKSUM = eae40bc29d06ee5e3078f9444fcea39b @@ -12,6 +26,11 @@ LUAJIT_SRC_DIR = $(realpath $(LUAJIT)/src) LUAJIT_CFLAGS = -I$(LUAJIT_SRC_DIR) LUAJIT_ARCHIVE = $(LUAJIT)/src/libluajit.a LUAJIT_URL = http://luajit.org/download/$(LUAJIT_VER).tar.gz +ifdef WIN32 + LUAJIT_PATCHES = \ + 001-luajit-ffi-win32-walk-all-modules.patch \ + 002-luajit-win32-build-static.patch +endif LPEG_VER = lpeg-0.10.2 LPEG_CHECKSUM = 1402433f02e37ddadff04a3d4118b026 @@ -19,6 +38,13 @@ LPEG = deps/$(LPEG_VER) LPEG_OBJECT = $(LPEG)/lpeg.o LPEG_URL = http://nordman.org/mirror/lpeg/$(LPEG_VER).tar.gz +MODPATH_ISS = deps/modpath.iss +MODPATH_ISS_CHECKSUM = a39bb32a15e0f4b90b38933e6ceb6a78 +MODPATH_ISS_URL = https://www.legroom.net/files/software/modpath.iss + +ISCC=iscc +CONVERT=convert + CFLAGS = -Wall -O2 -g $(LUAJIT_CFLAGS) $(GTK_CFLAGS) -DHOWL_PREFIX=$(PREFIX) ARCHIVES = $(LUAJIT_ARCHIVE) LIBS = -lm -ldl ${GTK_LIBS} @@ -33,17 +59,41 @@ ifeq ($(UNAME_S),Darwin) else LD_FLAGS = -Wl,-E endif +ifdef WIN32 + # MSYS sets CC to cc, even though it doesn't actually exist. + CC=gcc + CFLAGS += -mwindows + LIBS = -lm ${GTK_LIBS} -lstdc++ -lkernel32 -lgdi32 + LD_FLAGS = -mwindows -Wl,--export-all + ifdef MAKE_RC + WINRES=howl.res + endif +endif OBJECTS = main.o process_helpers.o DEP_OBJECTS = $(LPEG_OBJECT) -all: howl bytecode +ICON=howl.ico + +.PHONY: all bytecode deps-download deps-purge deps-clean clean install uninstall windist wininstall -howl: ${OBJECTS} main.h $(ARCHIVES) $(DEP_OBJECTS) Makefile - ${CC} -o howl ${OBJECTS} $(DEP_OBJECTS) ${ARCHIVES} ${LIBS} ${LD_FLAGS} +all: ${HOWL_EXE} bytecode + +${HOWL_EXE}: ${OBJECTS} main.h $(ARCHIVES) $(DEP_OBJECTS) Makefile $(WINRES) + ${CC} -o howl ${OBJECTS} $(DEP_OBJECTS) ${WINRES} ${ARCHIVES} ${LIBS} ${LD_FLAGS} ${OBJECTS}: %.o : %.c main.h $(LUAJIT) ${CC} -c $< ${CFLAGS} +${WINRES}: howl.rc ${ICON} + windres $< -O coff -o $@ + +# Doing the full conversion using ImageMagick gives nasty results. So, RSVG-Convert +# is used first, *then* ImageMagick. + +${ICON}: ../share/icons/hicolor/scalable/apps/howl.svg + RSVG-Convert $< -d 300 -p 300 -o howl.png + ${CONVERT} -background transparent howl.png -define icon:auto-resize=128,64,48,32,16 $@ + $(LPEG): @tools/download $(LPEG_URL) $(LPEG_CHECKSUM) tar xzf {file} -C deps @@ -53,14 +103,18 @@ $(LPEG_OBJECT): $(LPEG) $(LUAJIT) $(LUAJIT): @tools/download $(LUAJIT_URL) $(LUAJIT_CHECKSUM) tar xzf {file} -C deps @perl -piorig -e 's/LUA_IDSIZE\s*\d+/LUA_IDSIZE 120/' $(LUAJIT)/src/luaconf.h + cd ${LUAJIT} && for p in $(LUAJIT_PATCHES); do patch -Np1 -i $(CWD)/$$p; done $(LUAJIT_ARCHIVE): $(LUAJIT) cd ${LUAJIT} && $(MAKE) XCFLAGS="-DLUAJIT_ENABLE_LUA52COMPAT" +$(MODPATH_ISS): + @tools/download $(MODPATH_ISS_URL) $(MODPATH_ISS_CHECKSUM) cp {file} $(MODPATH_ISS) + deps-download: $(LUAJIT) $(LPEG) deps-purge: - rm -rf $(LUAJIT) $(LPEG) + rm -rf $(LUAJIT) $(LPEG) $(MODPATH_ISS) deps-clean: @rm $(LPEG_OBJECT) || true @@ -72,6 +126,7 @@ clean: bytecode: howl -@find ../lib ../bundles -name '*.bc' | xargs rm @find ../lib ../bundles -name '*.lua' -o -name '*.moon' | xargs ./howl --compile + @rm -rf windist wininst install: all @echo Installing to $(DESTDIR)$(PREFIX).. @@ -95,3 +150,21 @@ uninstall: @rm -v $(DESTDIR)$(PREFIX)/share/applications/howl.desktop @rm -v $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/howl.svg @echo All done. + +windist: + @rm -rf windist + @mkdir -p windist + + @$(MAKE) install PREFIX=windist MAKE_RC=1 + @for dll in `ldd howl.exe | grep mingw32 | cut -d= -f1 | cut -f2`; do \ + cp -v /mingw32/bin/$$dll windist/bin; \ + done + @rm windist/bin/howl-spec + @cp -v /mingw32/bin/librsvg*.dll /mingw32/bin/gspawn-win32-helper* windist/bin + @cp -v howl.exe windist/bin + @mkdir -p windist/lib + @cp -rv /mingw32/lib/gdk-pixbuf-2.0 windist/lib + @zip -r windist/howl.zip windist/bin windist/lib windist/share + +wininst: $(MODPATH_ISS) windist + $(ISCC) wininst.iss diff --git a/src/howl.rc b/src/howl.rc new file mode 100644 index 000000000..a67d2e9d6 --- /dev/null +++ b/src/howl.rc @@ -0,0 +1 @@ +id ICON "howl.ico" diff --git a/src/main.c b/src/main.c index aa701552c..ee0e00946 100644 --- a/src/main.c +++ b/src/main.c @@ -2,12 +2,18 @@ /* License: MIT (see LICENSE.md at the top-level directory of the distribution) */ #include "main.h" +#include #include #include #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) +#ifdef _WIN32 +#include +DWORD fr_private = FR_PRIVATE; +#endif + static void lua_run(int argc, char *argv[], const gchar *app_root, lua_State *L) { gchar *start_script; diff --git a/src/process_helpers.c b/src/process_helpers.c index 7902cbbb5..892734081 100644 --- a/src/process_helpers.c +++ b/src/process_helpers.c @@ -2,13 +2,56 @@ /* License: MIT (see LICENSE.md at the top-level directory of the distribution) */ #include +#ifndef _WIN32 #include +#else +#define WIFEXITED(w) ((w) < 128) +#define WEXITSTATUS(w) (w) +#define WIFSIGNALED(w) ((w) >= 128) +#define WTERMSIG(w) ((w) - 128) + +#define SIGHUP 1 /* hangup */ +#define SIGINT 2 /* interrupt */ +#define SIGQUIT 3 /* quit */ +#define SIGILL 4 /* illegal instruction (not reset when caught) */ +#define SIGTRAP 5 /* trace trap (not reset when caught) */ +#define SIGABRT 6 /* used by abort */ +#define SIGIOT SIGABRT /* synonym for SIGABRT on most systems */ +#define SIGEMT 7 /* EMT instruction */ +#define SIGFPE 8 /* floating point exception */ +#define SIGKILL 9 /* kill (cannot be caught or ignored) */ +#define SIGBUS 10 /* bus error */ +#define SIGSEGV 11 /* segmentation violation */ +#define SIGSYS 12 /* bad argument to system call */ +#define SIGPIPE 13 /* write on a pipe with no one to read it */ +#define SIGALRM 14 /* alarm clock */ +#define SIGTERM 15 /* software termination signal from kill */ +#define SIGURG 16 /* urgent condition on IO channel */ +#define SIGSTOP 17 /* sendable stop signal not from tty */ +#define SIGTSTP 18 /* stop signal from tty */ +#define SIGCONT 19 /* continue a stopped process */ +#define SIGCHLD 20 /* to parent on child stop or exit */ +#define SIGCLD 20 /* System V name for SIGCHLD */ +#define SIGTTIN 21 /* to readers pgrp upon background tty read */ +#define SIGTTOU 22 /* like TTIN for output if (tp->t_local<OSTOP) */ +#define SIGIO 23 /* input/output possible signal */ +#define SIGPOLL SIGIO /* System V name for SIGIO */ +#define SIGXCPU 24 /* exceeded CPU time limit */ +#define SIGXFSZ 25 /* exceeded file size limit */ +#define SIGVTALRM 26 /* virtual time alarm */ +#define SIGPROF 27 /* profiling time alarm */ +#define SIGWINCH 28 /* window changed */ +#define SIGLOST 29 /* resource lost (eg, record-lock lost) */ +#define SIGPWR SIGLOST /* power failure */ +#define SIGUSR1 30 /* user defined signal 1 */ +#define SIGUSR2 31 /* user defined signal 2 */ +#endif #include int process_exited_normally(int status) { return WIFEXITED(status); } int process_exit_status(int status) { return WEXITSTATUS(status); } -int process_was_signalled(int status) { return WIFSIGNALED(status); } -int process_get_term_sig(int status) { return WTERMSIG(status); } +int process_was_signalled(int status) { return WIFSIGNALED(status); } +int process_get_term_sig(int status) { return WTERMSIG(status); } int sig_HUP = SIGHUP; int sig_INT = SIGINT; diff --git a/src/wininst.iss b/src/wininst.iss new file mode 100644 index 000000000..d4373a05a --- /dev/null +++ b/src/wininst.iss @@ -0,0 +1,62 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "Howl" +#define MyAppVersion "0.4.1" +#define MyAppPublisher "The Howl Developers" +#define MyAppURL "http://howl.io/" +#define MyAppExeName "howl.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{F15DBA31-AA25-4F0F-83F4-D13588984724} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={pf}\{#MyAppName} +DisableWelcomePage=no +DisableProgramGroupPage=yes +DefaultGroupName=Howl +LicenseFile=../LICENSE.md +OutputDir=wininst +OutputBaseFilename=howl-setup +Compression=lzma +SolidCompression=yes +SetupIconFile=howl.ico + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "windist\bin\*"; DestDir: "{app}\bin"; Flags: recursesubdirs ignoreversion +Source: "windist\lib\*"; DestDir: "{app}\lib"; Flags: recursesubdirs ignoreversion +Source: "windist\share\*"; DestDir: "{app}\share"; Flags: recursesubdirs ignoreversion +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{commonprograms}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}" +Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\bin\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Code] +const + ModPathName = 'modifypath'; + ModPathType = 'system'; + +function ModPathDir(): TArrayOfString; +begin + setArrayLength(Result, 1) + Result[0] := ExpandConstant('{app}\bin'); +end; +#include "deps/modpath.iss" diff --git a/win.md b/win.md new file mode 100644 index 000000000..6f7e4cb7a --- /dev/null +++ b/win.md @@ -0,0 +1,5 @@ +#WIP + +mingw32/mingw-w64-i686-imagemagick + +version change: howl.rc, wininst.iss