Merge: Gamnit on iOS
authorJean Privat <jean@pryen.org>
Thu, 25 Jan 2018 20:16:37 +0000 (15:16 -0500)
committerJean Privat <jean@pryen.org>
Thu, 25 Jan 2018 20:16:37 +0000 (15:16 -0500)
Support iOS as a target for Gamnit games, including:

* GC on iOS and Xcode assets folder support in the compiler.
* iOS view controller and OpenGL view using the GLKit services.
* Loading of assets: sound, texture and text (and thus 3D models and fonts).
* iOS touch event and orientation change support.
* A few bug fix and tweaks to Gamnit.
* Minimal makefile rules to target iOS.

The game `asteronits` works as expected, here it is on iPad (With controls that are way too big):

![screen shot 2018-01-14 at 10 11 17](https://user-images.githubusercontent.com/208057/34918201-187b8b4a-f91d-11e7-86a2-6b891b237316.png)

Limitations:
* By default, the compiler generates an app for the simulator. To compile for a physical device, in practice, I run `nitc -m ios --compile-dir nit_compile` to generate the iOS project and view errors in Nit or in the custom Objective-C code. And then I launch Xcode with `open -a xcode nit_compile/ios/my_project.xcodeproj` to compile and run on a device from there.

* The depth API (3D) does not work on iOS with this PR because of known bugs in the current implementation. I have a rewrite of the depth API that fixes these bugs and bring a much needed performance boost.

* A `Sound` instance can't be played more than once concurrently. This could be improved by using `AVPlayer` as kind of sound channel and an `AVPlayerItem` for each sound instance, instead of the current `AVAudioPlayer`.

* iOS does not support exactly the same format for assets than Android. Sounds in mp3 format should work on both platforms. PNG files work on both platforms but iOS is more picky on the color palette, I've had an issue with a greyscale PNG, RGBA PNG files should be safe.

  You can always use the `app_files` annotation to include platform-specific assets.

* `gamnit::selection` does not work on iOS because we can't read the screen pixels. It should be updated to use an FBO.

* We still need a portable API for locking the screen orientation.

* Compiling Gamnit games for iOS fails at a missing pkg-config package. The package is not used and in can be safely ignored by removing the "pkgconfig" line in `lib/glesv2/glesv2.nit`. I'll need to find a clean fix for this...

Pull-Request: #2605
Reviewed-by: Romain Chanoir <romain.chanoir@viacesi.fr>

28 files changed:
contrib/action_nitro/Makefile
contrib/asteronits/.gitignore [new file with mode: 0644]
contrib/asteronits/Makefile
contrib/model_viewer/Makefile
lib/app/audio.nit
lib/gamnit/README.md
lib/gamnit/depth/shadow.nit
lib/gamnit/display.nit
lib/gamnit/display_ios.nit [new file with mode: 0644]
lib/gamnit/dynamic_resolution.nit
lib/gamnit/flat/flat_core.nit
lib/gamnit/gamnit.nit
lib/gamnit/gamnit_ios.nit [new file with mode: 0644]
lib/gamnit/input_ios.nit [new file with mode: 0644]
lib/glesv2/glesv2.nit
lib/ios/assets.nit [new file with mode: 0644]
lib/ios/audio.nit [new file with mode: 0644]
lib/ios/glkit.nit [new file with mode: 0644]
lib/pthreads/pthreads.nit
lib/realtime.nit
share/android-bdwgc/setup.sh
src/platform/ios.nit
src/platform/xcode_templates.nit
tests/Darwin.skip
tests/nitce.skip
tests/sav/test_platform_ios.res
tests/testfull.sh
tests/tests.sh

index 81db63b..22c9358 100644 (file)
@@ -10,6 +10,10 @@ bin/action_nitro.apk: $(shell nitls -M src/action_nitro.nit -m gamnit::android19
 android-release: $(shell nitls -M src/action_nitro.nit -m gamnit::android19 -m src/touch_ui.nit) pre-build android/res/
        nitc src/action_nitro.nit -m gamnit::android19 -m src/touch_ui.nit -o bin/action_nitro.apk --release
 
+ios: bin/action_nitro.app
+bin/action_nitro.app: $(shell nitls -M src/action_nitro.nit -m ios -m src/touch_ui.nit) pre-build
+       nitc src/action_nitro.nit -m ios -m src/touch_ui.nit -o $@ --compile-dir nit_compile
+
 src/gen/texts.nit: art/texts.svg
        make -C ../inkscape_tools/
        ../inkscape_tools/bin/svg_to_png_and_nit art/texts.svg -a assets/ -s src/gen/ -x 2.0 -g
diff --git a/contrib/asteronits/.gitignore b/contrib/asteronits/.gitignore
new file mode 100644 (file)
index 0000000..db11f0f
--- /dev/null
@@ -0,0 +1 @@
+/nit_compile
index aee8027..846c337 100644 (file)
@@ -3,7 +3,7 @@ NITLS=nitls
 
 all: bin/asteronits
 
-bin/asteronits: $(shell ${NITLS} -M src/asteronits.nit linux) pre-build
+bin/asteronits: $(shell ${NITLS} -M src/asteronits.nit -m linux) pre-build
        ${NITC} src/asteronits.nit -m linux -o $@
 
 bin/texture_atlas_parser: ../../lib/gamnit/texture_atlas_parser.nit
@@ -21,12 +21,23 @@ check: bin/asteronits
 # Android
 
 android: bin/asteronits.apk
-bin/asteronits.apk: $(shell ${NITLS} -M src/asteronits.nit android) android/res/ pre-build
+bin/asteronits.apk: $(shell ${NITLS} -M src/asteronits.nit -m android) android/res/ pre-build
        ${NITC} src/android.nit -m android -o $@
 
-android-release: $(shell ${NITLS} -M src/asteronits.nit android) android/res/ pre-build
+android-release: $(shell ${NITLS} -M src/asteronits.nit -m android) android/res/ pre-build
        ${NITC} src/android.nit -m android -o bin/asteronits.apk --release
 
 android/res/: art/icon.svg
        make -C ../inkscape_tools/
        ../inkscape_tools/bin/svg_to_icons --out android/res --android art/icon.svg
+
+# ---
+# iOS
+
+ios: bin/asteronits.app
+bin/asteronits.app: $(shell ${NITLS} -M src/asteronits.nit -m ios -m src/touch_ui.nit) pre-build ios/AppIcon.appiconset/Contents.json
+       ${NITC} src/asteronits.nit -m ios -m src/touch_ui.nit -o $@ --compile-dir nit_compile
+
+ios/AppIcon.appiconset/Contents.json: art/icon.svg
+       mkdir -p ios
+       ../../contrib/inkscape_tools/bin/svg_to_icons art/icon.svg --ios --out ios/AppIcon.appiconset/
index de05fa9..d64fbaa 100644 (file)
@@ -13,10 +13,10 @@ check: bin/model_viewer
 # Android
 
 android: bin/model_viewer.apk
-bin/model_viewer.apk: $(shell ${NITLS} -M src/model_viewer.nit android) android/res/
+bin/model_viewer.apk: $(shell ${NITLS} -M src/model_viewer.nit -m android) android/res/
        ${NITC} src/model_viewer.nit -m android -o $@
 
-android-release: $(shell ${NITLS} -M src/model_viewer.nit android) android/res/
+android-release: $(shell ${NITLS} -M src/model_viewer.nit -m android) android/res/
        ${NITC} src/model_viewer.nit -m android -o bin/model_viewer.apk --release
 
 android/res/: art/icon.png
@@ -37,3 +37,10 @@ android/libs/cardboard.jar:
        mkdir -p android/libs
        curl --progress-bar -o android/libs/cardboard.jar \
        https://raw.githubusercontent.com/googlevr/gvr-android-sdk/e226f15c/CardboardSample/libs/cardboard.jar
+
+# ---
+# iOS
+
+ios: bin/model_viewer.app
+bin/model_viewer.app: $(shell ${NITLS} -M src/model_viewer.nit -m ios)
+       ${NITC} src/model_viewer.nit -m ios -o $@ --compile-dir nit_compile
index b26ae7b..87b510f 100644 (file)
@@ -27,6 +27,7 @@ import core::error
 # Platform variations
 import linux::audio is conditional(linux)
 import android::audio is conditional(android)
+import ios::audio is conditional(ios)
 
 # Abstraction of a playable Audio
 abstract class PlayableAudio
index dba4c4e..68370d9 100644 (file)
@@ -6,18 +6,25 @@ It is based on the portability framework _app.nit_ and the OpenGL ES 2.0 standar
 # System configuration
 
 To compile the _gamnit_ apps packaged with the Nit repository on GNU/Linux you need to install the dev version of a few libraries and some tools.
-Under Debian 8.2, this command should install everything needed:
+On Debian 8.2, this command should install everything needed:
 
 ~~~
 apt-get install libgles2-mesa-dev libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev inkscape
 ~~~
 
-Under Windows 64 bits, using msys2, you can install the required packages with:
+On Windows 64 bits, using msys2, you can install the required packages with:
 
 ~~~
 pacman -S mingw-w64-x86_64-angleproject-git mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer
 ~~~
 
+While macOS isn't supported, it can create iOS apps.
+You need to install and setup Xcode, and you may install the GLSL shader validation tool via `brew`:
+
+~~~
+brew install glslang
+~~~
+
 # Services by submodules
 
 _gamnit_ is modular, different services of the framework are available through different submodules:
index 75178f5..4dcae8c 100644 (file)
@@ -112,7 +112,7 @@ redef class App
                end
 
                # Take down, bring back default values
-               glBindFramebuffer(gl_FRAMEBUFFER, shadow_context.screen_framebuffer)
+               bind_screen_framebuffer shadow_context.screen_framebuffer
                glColorMask(true, true, true, true)
        end
 
@@ -218,7 +218,6 @@ private class ShadowContext
                assert gl_error == gl_NO_ERROR else print_error gl_error
 
                resize(display, shadow_resolution)
-               assert glCheckFramebufferStatus(gl_FRAMEBUFFER) == gl_FRAMEBUFFER_COMPLETE
 
                # Array buffer
                buffer_array = glGenBuffers(1).first
index 868b359..0d78907 100644 (file)
@@ -20,6 +20,7 @@ import mnit::input
 
 import display_linux is conditional(linux)
 import display_android is conditional(android)
+import display_ios is conditional(ios)
 
 # Should Gamnit be more verbose?
 fun debug_gamnit: Bool do return false
diff --git a/lib/gamnit/display_ios.nit b/lib/gamnit/display_ios.nit
new file mode 100644 (file)
index 0000000..3b8cfdb
--- /dev/null
@@ -0,0 +1,111 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Gamnit display implementation for iOS
+module display_ios
+
+import ios
+import ios::glkit
+intrude import ios::assets
+intrude import textures
+
+in "ObjC" `{
+       #import <GLKit/GLKit.h>
+       #import <OpenGLES/ES2/gl.h>
+`}
+
+redef class GamnitDisplay
+
+       redef var width = 200
+       redef var height = 300
+
+       # Underlying GLKit game controller and view
+       var glk_view: NitGLKView is noautoinit
+
+       redef fun setup
+       do
+               var view = new GamnitGLKView
+               view.multiple_touch_enabled = true
+               self.glk_view = view
+               self.width = view.drawable_width
+               self.height = view.drawable_height
+       end
+end
+
+# View controller implemented by gamnit
+class GamnitGLKView
+       super NitGLKView
+end
+
+redef class TextureAsset
+       redef fun load_from_platform
+       do
+               var error = glGetError
+               assert error == gl_NO_ERROR else print_error error
+
+               # Find file
+               var ns_path = ("assets"/path).to_nsstring
+               var path_in_bundle = asset_path(ns_path)
+               if path_in_bundle.address_is_null then
+                       self.error = new Error("Texture at '{path}' not found")
+                       return
+               end
+
+               # Load texture
+               var glk_texture = glkit_load(path_in_bundle, premultiply_alpha)
+               if glk_texture.address_is_null then
+                       self.error = new Error("Failed to load texture at '{self.path}'")
+                       return
+               end
+
+               gl_texture = glk_texture.name
+               width = glk_texture.width.to_f
+               height = glk_texture.height.to_f
+               loaded = true
+
+               error = glGetError
+               assert error == gl_NO_ERROR
+       end
+
+       # Load image at `path` with GLKit services
+       private fun glkit_load(path: NSString, premultiply: Bool): GLKTextureInfo
+       in "ObjC" `{
+
+               // The premultiplication flag has been inverted between iOS 9 and 10
+               NSNumber *premultiply_opt;
+               NSComparisonResult order = [[UIDevice currentDevice].systemVersion compare: @"10.0.0" options: NSNumericSearch];
+               if (order == NSOrderedSame || order == NSOrderedDescending) {
+                       // >= 10
+                       premultiply_opt = premultiply? @NO: @YES;
+               } else {
+                       // < 10
+                       premultiply_opt = premultiply? @YES: @NO;
+               }
+
+               NSDictionary *options = @{GLKTextureLoaderApplyPremultiplication: premultiply_opt};
+               NSError *error;
+               GLKTextureInfo *spriteTexture = [GLKTextureLoader textureWithContentsOfFile: path options: options error: &error];
+               if (error != nil) NSLog(@"Failed to load texture: %@", [error localizedDescription]); // TODO return details to Nit
+
+               return spriteTexture;
+       `}
+end
+
+private extern class GLKTextureInfo in "ObjC" `{ GLKTextureInfo * `}
+       super NSObject
+
+       fun name: Int in "ObjC" `{ return self.name; `}
+       fun width: Int in "ObjC" `{ return self.width; `}
+       fun height: Int in "ObjC" `{ return self.height; `}
+end
index 0219db4..53a44e8 100644 (file)
@@ -54,7 +54,21 @@ redef class App
 
        private var perf_clock_dynamic_resolution = new Clock is lazy
 
-       redef fun create_scene
+       # Real screen framebuffer
+       private var screen_framebuffer_cache: Int = -1
+
+       # Real screen framebuffer name
+       fun screen_framebuffer: Int
+       do
+               var cache = screen_framebuffer_cache
+               if cache != -1 then return cache
+
+               cache = glGetIntegerv(gl_FRAMEBUFFER_BINDING, 0)
+               self.screen_framebuffer_cache = cache
+               return cache
+       end
+
+       redef fun create_gamnit
        do
                super
 
@@ -62,11 +76,13 @@ redef class App
                program.compile_and_link
                var error = program.error
                assert error == null else print_error error
+
+               dynamic_context_cache = null
        end
 
        redef fun on_resize(display)
        do
-               dynamic_context.resize(display, max_dynamic_resolution_ratio)
+               if dynamic_context_cache != null then dynamic_context.resize(display, max_dynamic_resolution_ratio)
                super
        end
 
@@ -77,7 +93,7 @@ redef class App
 
                if dynamic_resolution_ratio == 1.0 then
                        # Draw directly to the screen framebuffer
-                       glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.screen_framebuffer)
+                       bind_screen_framebuffer screen_framebuffer
                        glViewport(0, 0, display.width, display.height)
                        glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
 
@@ -109,7 +125,7 @@ redef class App
                var ratio = dynamic_resolution_ratio
                ratio = ratio.clamp(min_dynamic_resolution_ratio, max_dynamic_resolution_ratio)
 
-               glBindFramebuffer(gl_FRAMEBUFFER, dynamic_context.screen_framebuffer)
+               bind_screen_framebuffer screen_framebuffer
                glBindBuffer(gl_ARRAY_BUFFER, dynamic_context.buffer_array)
                glViewport(0, 0, display.width, display.height)
                glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
@@ -148,7 +164,17 @@ redef class App
        end
 
        # Framebuffer and texture for dynamic resolution intermediate drawing
-       private var dynamic_context: DynamicContext = create_dynamic_context is lazy
+       private fun dynamic_context: DynamicContext
+       do
+               var cache = dynamic_context_cache
+               if cache != null then return cache
+
+               cache = create_dynamic_context
+               dynamic_context_cache = cache
+               return cache
+       end
+
+       private var dynamic_context_cache: nullable DynamicContext = null
 
        private fun create_dynamic_context: DynamicContext
        do
@@ -166,9 +192,6 @@ end
 # Handles to reused GL buffers and texture
 private class DynamicContext
 
-       # Real screen framebuffer
-       var screen_framebuffer: Int = -1
-
        # Dynamic screen framebuffer
        var dynamic_framebuffer: Int = -1
 
@@ -186,10 +209,6 @@ private class DynamicContext
        do
                # TODO enable antialiasing.
 
-               # Set aside the real screen framebuffer name
-               var screen_framebuffer = glGetIntegerv(gl_FRAMEBUFFER_BINDING, 0)
-               self.screen_framebuffer = screen_framebuffer
-
                # Framebuffer
                var framebuffer = glGenFramebuffers(1).first
                glBindFramebuffer(gl_FRAMEBUFFER, framebuffer)
index 59f4b37..ef49d00 100644 (file)
@@ -476,9 +476,6 @@ redef class App
        do
                super
 
-               # Clean up
-               simple_2d_program.delete
-
                # Close gamnit
                var display = display
                if display != null then display.close
index a21ffe9..c1ecc86 100644 (file)
@@ -23,6 +23,8 @@ import programs
 
 import gamnit_android is conditional(android)
 import gamnit_linux is conditional(linux)
+import gamnit_ios is conditional(ios)
+import input_ios is conditional(ios)
 
 redef class App
 
@@ -111,3 +113,8 @@ redef class App
        # The framework handles resizing the viewport automatically.
        fun on_resize(display: GamnitDisplay) do end
 end
+
+# Portable indirection to `glBindFramebuffer(gl_FRAMEBUFFER, fbo)`
+#
+# This is implemented differently on iOS.
+fun bind_screen_framebuffer(fbo: Int) do glBindFramebuffer(gl_FRAMEBUFFER, fbo)
diff --git a/lib/gamnit/gamnit_ios.nit b/lib/gamnit/gamnit_ios.nit
new file mode 100644 (file)
index 0000000..4ee0a98
--- /dev/null
@@ -0,0 +1,46 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Support services for gamnit on iOS
+module gamnit_ios
+
+import ios
+import gamnit
+
+import ios::assets
+
+redef class App
+       redef fun did_finish_launching_with_options
+       do
+               create_gamnit
+               create_scene
+               return super
+       end
+
+       # Disable the game loop to rely on the GLKView callbacks on each frame instead
+       redef fun run do end
+
+       private fun frame_full_indirect do frame_full
+end
+
+redef class GamnitGLKView
+       redef fun update do app.frame_full_indirect
+end
+
+redef fun bind_screen_framebuffer(fbo)
+do
+       var display = app.display
+       assert display != null
+       display.glk_view.bind_drawable
+end
diff --git a/lib/gamnit/input_ios.nit b/lib/gamnit/input_ios.nit
new file mode 100644 (file)
index 0000000..cb3d463
--- /dev/null
@@ -0,0 +1,59 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Gamnit event support for iOS
+module input_ios
+
+intrude import ios::glkit
+import display_ios
+import gamnit_ios
+
+# Pointer/touch event on iOS
+class GamnitIOSPointerEvent
+       super PointerEvent
+
+       private var native: UIEvent
+
+       private var native_touch: UITouch
+
+       private var content_scale_factor: Float
+
+       redef fun x do return native_touch.x * content_scale_factor
+
+       redef fun y do return native_touch.y * content_scale_factor
+
+       redef var pressed
+
+       redef var is_move
+
+       redef var pointer_id = native_touch.to_i is lazy
+end
+
+redef class NitGLKView
+
+       redef var content_scale_factor = super is lazy
+
+       redef fun touches_began(touches, event)
+       do app.accept_event(new GamnitIOSPointerEvent(event, touches.any_object, content_scale_factor, true, false))
+
+       redef fun touches_moved(touches, event)
+       do app.accept_event(new GamnitIOSPointerEvent(event, touches.any_object, content_scale_factor, true, true))
+
+       redef fun touches_ended(touches, event)
+       do app.accept_event(new GamnitIOSPointerEvent(event, touches.any_object, content_scale_factor, false, false))
+
+       # TODO handle cancel
+       #redef fun touches_cancelled(touches_event) do
+       #do app.accept_event(new GamnitIOSPointerEvent(event, false, false))
+end
index 0520c9d..c70f631 100644 (file)
@@ -41,7 +41,11 @@ import android::aware
 intrude import c
 
 in "C Header" `{
+#ifdef __APPLE__
+       #include <OpenGLES/ES2/gl.h>
+#else
        #include <GLES2/gl2.h>
+#endif
 `}
 
 # OpenGL ES program to which we attach shaders
diff --git a/lib/ios/assets.nit b/lib/ios/assets.nit
new file mode 100644 (file)
index 0000000..4c4ed0e
--- /dev/null
@@ -0,0 +1,55 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Implementation of `app::assets`
+module assets
+
+import cocoa
+import app::assets
+
+redef class TextAsset
+       redef fun load
+       do
+               # Find file
+               var ns_path = ("assets"/path).to_nsstring
+               var path_in_bundle = asset_path(ns_path)
+               if path_in_bundle.address_is_null then
+                       self.error = new Error("TextAsset at '{path}' not found")
+                       self.to_s = ""
+                       return ""
+               end
+
+               # Load content
+               var text = asset_content(path_in_bundle)
+               if text.address_is_null then
+                       self.error = new Error("Failed to read content of TextAsset at '{path}'")
+                       self.to_s = ""
+                       return ""
+               end
+
+               return text.to_s
+       end
+end
+
+private fun asset_path(path: NSString): NSString in "ObjC" `{
+       return [[NSBundle mainBundle] pathForResource:path ofType:nil];
+`}
+
+private fun asset_url(path: NSString): NSObject in "ObjC" `{
+       return [[NSBundle mainBundle] URLForResource:path withExtension:nil];
+`}
+
+private fun asset_content(path: NSString): NSString in "ObjC" `{
+       return [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
+`}
diff --git a/lib/ios/audio.nit b/lib/ios/audio.nit
new file mode 100644 (file)
index 0000000..3cead88
--- /dev/null
@@ -0,0 +1,95 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# iOS implementation of `app::audio` using `AVAudioPlayer`
+module audio
+
+import app::audio
+intrude import ios::assets
+
+in "ObjC Header" `{
+       #import <AVFoundation/AVFoundation.h>
+`}
+
+redef class PlayableAudio
+
+       redef var error = null
+
+       private var native: nullable AVAudioPlayer is lazy do
+
+               # Find file
+               var ns_path = ("assets"/path).to_nsstring
+               var url_in_bundle = asset_url(ns_path)
+               if url_in_bundle.address_is_null then
+                       self.error = new Error("Sound at '{path}' not found")
+                       return null
+               end
+
+               var player = new AVAudioPlayer.contents_of(url_in_bundle)
+               # TODO set delegate to get further errors
+               if player.address_is_null then
+                       self.error = new Error("Sound at '{path}' failed to load")
+                       return null
+               end
+
+               player.prepare_to_play
+
+               return player
+       end
+
+       redef fun load do native # For lazy loading
+
+       redef fun play
+       do
+               var native = native
+               if native != null then native.play_and_repare_async
+       end
+
+       # Free native resources
+       fun destroy
+       do
+               var native = native
+               if native != null then native.release
+       end
+end
+
+# Audio player playing audio from a file or from memory
+private extern class AVAudioPlayer in "ObjC" `{ AVAudioPlayer *`}
+       super NSObject
+
+       new contents_of(url: NSObject) in "ObjC" `{
+               NSError *error;
+               AVAudioPlayer *a = [[AVAudioPlayer alloc] initWithContentsOfURL:(NSURL*)url error:&error];
+               if (error != nil) {
+                       NSLog(@"Failed to load sound: %@", [error localizedDescription]);
+                       return NULL;
+               }
+
+               return (__bridge AVAudioPlayer*)CFBridgingRetain(a);
+       `}
+
+       fun play in "ObjC" `{ [self play]; `}
+
+       fun prepare_to_play in "ObjC" `{ [self prepareToPlay]; `}
+
+       fun play_and_repare_async in "ObjC" `{
+               dispatch_queue_t q = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0);
+               dispatch_async(q, ^{
+                       [self play];
+                       [self prepareToPlay];
+               });
+       `}
+
+       fun release in "ObjC" `{ CFBridgingRelease((__bridge void*)self); `}
+end
diff --git a/lib/ios/glkit.nit b/lib/ios/glkit.nit
new file mode 100644 (file)
index 0000000..db1bb7a
--- /dev/null
@@ -0,0 +1,204 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# GLKit services to create an OpenGL ES context on iOS
+module glkit
+
+#import glesv2
+import ios
+
+in "ObjC Header" `{
+       #import <GLKit/GLKit.h>
+
+       // Nit controller for games
+       @interface GameViewController : GLKViewController
+
+               // Nit object receiving callbacks
+               @property void* nit_glk_view;
+       @end
+`}
+
+in "ObjC" `{
+
+       @implementation GameViewController
+
+               - (void)update
+               {
+                       NitGLKView_update((NitGLKView)self.nit_glk_view);
+               }
+
+               - (UIInterfaceOrientationMask)supportedInterfaceOrientations
+               {
+                       long res = NitGLKView_supported_interface_orientations((NitGLKView)self.nit_glk_view);
+                       if (res == 0) return [super supportedInterfaceOrientations];
+                       return (UIInterfaceOrientationMask)res;
+               }
+
+               - (BOOL)shouldAutorotate
+               {
+                       return NitGLKView_should_autorotate((NitGLKView)self.nit_glk_view);
+               }
+
+               - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
+               {
+                       NitGLKView_touches_began((NitGLKView)self.nit_glk_view, touches, event);
+               }
+
+               - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
+               {
+                       NitGLKView_touches_moved((NitGLKView)self.nit_glk_view, touches, event);
+               }
+
+               - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
+               {
+                       NitGLKView_touches_ended((NitGLKView)self.nit_glk_view, touches, event);
+               }
+
+               - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
+               {
+                       NitGLKView_touches_cancelled((NitGLKView)self.nit_glk_view, touches, event);
+               }
+
+               - (void)viewWillTransitionToSize:(CGSize)size
+                 withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
+                       NitGLKView_view_will_transition_to_size((NitGLKView)self.nit_glk_view, size.width, size.height);
+               }
+        @end
+`}
+
+# Wrapper for both an Objective-C `GLKViewController` and its `GLKView`
+private extern class NativeGLKViewController in "ObjC" `{ GLKViewController * `}
+       super NSObject
+
+       fun content_scale_factor: Float in "ObjC" `{ return self.view.contentScaleFactor; `}
+       fun drawable_width: Int in "ObjC" `{ return ((GLKView*)self.view).drawableWidth; `}
+       fun drawable_height: Int in "ObjC" `{ return ((GLKView*)self.view).drawableHeight; `}
+       fun bind_drawable in "ObjC" `{ [((GLKView*)self.view) bindDrawable]; `}
+
+       fun multiple_touch_enabled: Bool in "ObjC" `{ return [self.view isMultipleTouchEnabled]; `}
+       fun multiple_touch_enabled=(val: Bool) in "ObjC" `{ return [self.view setMultipleTouchEnabled: val]; `}
+end
+
+# OpenGL view controller
+class NitGLKView
+       private var native: NativeGLKViewController = setup(app.app_delegate)
+
+       # Scale factor from logical coordinate space to the device coordinate space
+       fun content_scale_factor: Float do return native.content_scale_factor
+
+       # Width of the underlying framebuffer
+       fun drawable_width: Int do return native.drawable_width
+
+       # Height of the underlying framebuffer
+       fun drawable_height: Int do return native.drawable_height
+
+       # Bind the view framebuffer
+       fun bind_drawable do native.bind_drawable
+
+       private fun setup(app_delegate: AppDelegate): NativeGLKViewController
+       import touches_began, touches_moved, touches_ended, touches_cancelled,
+       update, should_autorotate, supported_interface_orientations,
+       view_will_transition_to_size in "ObjC" `{
+
+               app_delegate.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
+               app_delegate.window.backgroundColor = [UIColor whiteColor]; // TODO make configurable
+
+               // Create EAGL context and view
+               EAGLContext * context = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
+               GLKView *view = [[GLKView alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
+               view.context = context;
+
+               // Ask for antialiasing
+               view.drawableMultisample = GLKViewDrawableMultisample4X;
+               view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
+
+               GameViewController *cont = [[GameViewController alloc] init];
+               cont.view = view;
+
+               // Setup callbacks
+               NitGLKView_incr_ref(self);
+               cont.nit_glk_view = self;
+
+               // Make our controller the root
+               view.delegate = cont;
+               [app_delegate.window setRootViewController: cont];
+
+               // Enable the context
+               [app_delegate.window makeKeyAndVisible];
+               [EAGLContext setCurrentContext: context];
+               [view bindDrawable];
+
+               return cont;
+       `}
+
+       # Is multi-touch supported?
+       fun multiple_touch_enabled: Bool do return native.multiple_touch_enabled
+
+       # Is multi-touch supported?
+       fun multiple_touch_enabled=(val: Bool) do native.multiple_touch_enabled = val
+
+       # Should the view auto rotate to follow the device orientation?
+       #
+       # Defaults to `true`.
+       fun should_autorotate: Bool do return true
+
+       # If `should_autorotate`, what are the supported interface orientations?
+       #
+       # Redef to return values of Objective-C `UIInterfaceOrientationMask`
+       fun supported_interface_orientations: Int do return 0
+
+       # Hook to update the view content, called once per frame
+       fun update do end
+
+       # Hook on a new touch event
+       fun touches_began(touches: NSSet_UITouch, event: UIEvent) do end
+
+       # Hook when a touch moves
+       fun touches_moved(touches: NSSet_UITouch, event: UIEvent) do end
+
+       # Hook on the end of a touch event
+       fun touches_ended(touches: NSSet_UITouch, event: UIEvent) do end
+
+       # Hook on a touch event cancellation
+       fun touches_cancelled(touches: NSSet_UITouch, event: UIEvent) do end
+
+       # Hook when size of the view is about to change to `width` by `height`
+       fun view_will_transition_to_size(width, height: Float) do end
+end
+
+# UIKit event
+extern class UIEvent in "ObjC" `{ UIEvent * `}
+       super NSObject
+end
+
+# Objective-C `NSSet` of `UITouch`
+extern class NSSet_UITouch in "ObjC" `{ NSSet<UITouch*>* `}
+       super NSObject
+
+       # Get any object of this set
+       fun any_object: UITouch in "ObjC" `{ return [self anyObject]; `}
+end
+
+# UIKit touch event
+extern class UITouch in "ObjC" `{ UITouch * `}
+
+       # X coordinate
+       fun x: Float in "ObjC" `{ return [self locationInView:self.view].x; `}
+
+       # Y coordinate
+       fun y: Float in "ObjC" `{ return [self locationInView:self.view].y; `}
+
+       # Address of this object as an integer for identity detection
+       fun to_i: Int in "ObjC" `{ return (long)self; `}
+end
index 3a6dc7b..91ebb94 100644 (file)
@@ -38,17 +38,8 @@ in "C" `{
        // TODO protect with: #ifdef WITH_LIBGC
        // We might have to add the next line to gc_chooser.c too, especially
        // if we get an error like "thread not registered with GC".
-       #ifdef __APPLE__
-               #include "TargetConditionals.h"
-               #if TARGET_OS_IPHONE == 1
-                       #define IOS
-               #endif
-       #endif
-
-       #if !defined(IOS)
-               #define GC_THREADS
-               #include <gc.h>
-       #endif
+       #define GC_THREADS
+       #include <gc.h>
 `}
 
 redef class Sys
index 33708b6..31040ba 100644 (file)
@@ -23,15 +23,25 @@ in "C header" `{
 
 in "C" `{
 
-#if defined(__MACH__) && !defined(CLOCK_REALTIME)
+#ifdef __APPLE__
+       #include <TargetConditionals.h>
+       #if defined(TARGET_OS_IPHONE) && __IPHONE_OS_VERSION_MIN_REQUIRED < 100000
+               // Preserve compatibility with pre-iOS 10 devices where there is no clock_get_time.
+               #undef CLOCK_REALTIME
+       #endif
+#endif
+
+#if (defined(__MACH__) || defined(TARGET_OS_IPHONE)) && !defined(CLOCK_REALTIME)
 /* OS X does not have clock_gettime, mascarade it and use clock_get_time
  * cf http://stackoverflow.com/questions/11680461/monotonic-clock-on-osx
 */
 #include <mach/clock.h>
 #include <mach/mach.h>
+#undef CLOCK_REALTIME
+#undef CLOCK_MONOTONIC
 #define CLOCK_REALTIME CALENDAR_CLOCK
 #define CLOCK_MONOTONIC SYSTEM_CLOCK
-void clock_gettime(clock_t clock_name, struct timespec *ts) {
+void nit_clock_gettime(clock_t clock_name, struct timespec *ts) {
        clock_serv_t cclock;
        mach_timespec_t mts;
        host_get_clock_service(mach_host_self(), clock_name, &cclock);
@@ -40,6 +50,8 @@ void clock_gettime(clock_t clock_name, struct timespec *ts) {
        ts->tv_sec = mts.tv_sec;
        ts->tv_nsec = mts.tv_nsec;
 }
+#else
+       #define nit_clock_gettime clock_gettime
 #endif
 `}
 
@@ -56,7 +68,7 @@ private extern class Timespec `{struct timespec*`}
        # Init a new Timespec from now.
        new monotonic_now `{
                struct timespec* tv = malloc( sizeof(struct timespec) );
-               clock_gettime( CLOCK_MONOTONIC, tv );
+               nit_clock_gettime( CLOCK_MONOTONIC, tv );
                return tv;
        `}
 
@@ -70,7 +82,7 @@ private extern class Timespec `{struct timespec*`}
 
        # Update `self` clock.
        fun update `{
-               clock_gettime(CLOCK_MONOTONIC, self);
+               nit_clock_gettime(CLOCK_MONOTONIC, self);
        `}
 
        # Subtract `other` from `self`
@@ -165,7 +177,7 @@ class Clock
        # Smallest time frame reported by clock
        private fun resolution: Timespec `{
                struct timespec* tv = malloc( sizeof(struct timespec) );
-#if defined(__MACH__) && !defined(CLOCK_REALTIME)
+#if (defined(__MACH__) || defined(TARGET_OS_IPHONE)) && !defined(CLOCK_REALTIME)
                clock_serv_t cclock;
                int nsecs;
                mach_msg_type_number_t count;
index bcd931d..fcfbab3 100755 (executable)
 
 # Fetch libgc/bdwgc
 
-# cd to the absolute installation path
-if expr match "$0" "^/.*"; then
-       install="`dirname "$0"`"
-else
-       install="`pwd`/`dirname "$0"`"
-fi
-cd $install
+# cd to the installation path
+cd "`dirname "${BASH_SOURCE[0]}"`"
 
 # Download or redownload
 rm -rf bdwgc
index 0cb1ce0..27979b0 100644 (file)
@@ -32,7 +32,7 @@ private class IOSPlatform
        super Platform
 
        redef fun supports_libunwind do return false
-       redef fun supports_libgc do return false
+       redef fun supports_libgc do return true
        redef fun toolchain(toolcontext, compiler) do return new IOSToolchain(toolcontext, compiler)
 end
 
@@ -53,6 +53,8 @@ private class IOSToolchain
 
        redef fun default_outname do return "{super}.app"
 
+       private var bdwgc_dir: nullable String = null
+
        # Compile C files in `ios_project_root/app_project.name`
        redef fun compile_dir
        do
@@ -66,6 +68,20 @@ private class IOSToolchain
                if ios_project_root.file_exists then ios_project_root.rmdir
                compile_dir.mkdir
 
+               # Download the libgc/bdwgc sources
+               var nit_dir = toolcontext.nit_dir or else "."
+               var share_dir = (nit_dir/"share").realpath
+               if not share_dir.file_exists then
+                       print "iOS project error: Nit share directory not found, please use the environment variable NIT_DIR"
+                       exit 1
+               end
+
+               var bdwgc_dir = "{share_dir}/android-bdwgc/bdwgc"
+               self.bdwgc_dir = bdwgc_dir
+               if not bdwgc_dir.file_exists then
+                       toolcontext.exec_and_check(["{share_dir}/android-bdwgc/setup.sh"], "iOS project error")
+               end
+
                super
        end
 
@@ -101,16 +117,10 @@ private class IOSToolchain
 
                var icons_found = false
 
-               for path in app_files do
-                       var icon_dir = path / "ios" / "AppIcon.appiconset"
-                       if icon_dir.file_exists then
-                               icons_found = true
-
-                               # Prepare the `Assets.xcassets` folder
-                               var target_assets_dir = compile_dir / "Assets.xcassets"
-                               if not target_assets_dir.file_exists then target_assets_dir.mkdir
-
-                               """
+               # Prepare the `Assets.xcassets` folder
+               var target_assets_dir = compile_dir / "Assets.xcassets"
+               if not target_assets_dir.file_exists then target_assets_dir.mkdir
+               """
 {
   "info" : {
        "version" : 1,
@@ -118,13 +128,28 @@ private class IOSToolchain
   }
 }""".write_to_file target_assets_dir / "Contents.json"
 
+               (compile_dir / "assets").mkdir
+
+               for path in app_files do
+
+                       # Icon
+                       var icon_dir = path / "ios" / "AppIcon.appiconset"
+                       if icon_dir.file_exists then
+                               icons_found = true
+
+
                                # copy the res folder to the compile dir
                                icon_dir = icon_dir.realpath
                                toolcontext.exec_and_check(["cp", "-R", icon_dir, target_assets_dir], "iOS project error")
                        end
-               end
 
-               # TODO Register asset files
+                       # Assets
+                       var assets_dir = path / "assets"
+                       if assets_dir.file_exists then
+                               assets_dir = assets_dir.realpath
+                               toolcontext.exec_and_check(["cp", "-r", assets_dir, compile_dir], "iOS project error")
+                       end
+               end
 
                # ---
                # project_folder.xcodeproj (projet meta data)
@@ -142,6 +167,20 @@ private class IOSToolchain
                        pbx.add_file new PbxFile(file.filename.basename)
                end
 
+               # GC
+               if compiler.target_platform.supports_libgc then
+                       var bdwgc_dir = bdwgc_dir
+                       assert bdwgc_dir != null
+
+                       pbx.cflags = "-I '{bdwgc_dir}/include/' -I '{bdwgc_dir}/libatomic_ops/src' -fno-strict-aliasing " +
+                       "-DWITH_LIBGC -DNO_EXECUTE_PERMISSION -DALL_INTERIOR_POINTERS -DGC_NO_THREADS_DISCOVERY -DNO_DYLD_BIND_FULLY_IMAGE " +
+                       "-DGC_DISABLE_INCREMENTAL -DGC_THREADS -DUSE_MMAP -DUSE_MUNMAP -DGC_GCJ_SUPPORT -DJAVA_FINALIZATION "
+
+                       var gc_file = new PbxFile("{bdwgc_dir}/extra/gc.c")
+                       gc_file.cflags = "-Wno-tautological-pointer-compare"
+                       pbx.add_file gc_file
+               end
+
                # Basic storyboard, mainly to have the right screen size
                var launch_screen_storyboard = new LaunchScreenStoryboardTemplate
                launch_screen_storyboard.title = app_project.name
index 776b656..62768a2 100644 (file)
@@ -85,6 +85,9 @@ class PbxFile
        # Path to `self`
        var path: String
 
+       # Compiler flags for this source file
+       var cflags: String = "" is writable
+
        # UUID for build elements
        private var build_uuid: String = sys.pbx_uuid_generator.next_uuid is lazy
 
@@ -104,15 +107,22 @@ class PbxFile
        end
 
        # PBX description of this file
-       private fun description: Writable do return """
+       private fun description: Writable
+       do
+               var extra = ""
+               var cflags = cflags
+               if not cflags.is_empty then extra = "\nsettings = \{COMPILER_FLAGS = \"{cflags}\"; \};"
+
+               return """
                {{{ref_uuid}}} /* {{{doc}}} */ = {
                        isa = PBXFileReference;
                        fileEncoding = 4;
                        lastKnownFileType = {{{file_type}}};
-                       path = {{{path}}};
-                       sourceTree = "<group>";
+                       path = '{{{path}}}';
+                       sourceTree = "<group>";{{{extra}}}
                        };
 """
+       end
 
        private fun add_to_project(project: PbxprojectTemplate)
        do
@@ -139,6 +149,9 @@ class PbxprojectTemplate
        # Name of the project
        var name: String
 
+       # OTHER_CFLAGS
+       var cflags = "" is writable
+
        # All body/implementation source files to be compiled
        private var source_files = new Array[PbxFile]
 
@@ -174,6 +187,7 @@ class PbxprojectTemplate
 """
 
                add """
+               0F4688411FDF8748004F34D4 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 0F4688401FDF8748004F34D4 /* assets */; };
                0FDD07A21C6F8E0E006FF70E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0FDD07A11C6F8E0E006FF70E /* LaunchScreen.storyboard */; };
 /* End PBXBuildFile section */
 
@@ -219,6 +233,7 @@ class PbxprojectTemplate
                for file in files do add file.description
 
                add """
+               0F4688401FDF8748004F34D4 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = {{{name}}}/assets; sourceTree = SOURCE_ROOT; };
                0FDD07A11C6F8E0E006FF70E /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -253,6 +268,7 @@ class PbxprojectTemplate
                AF9F83CE1A5F0D21004B62C0 /* {{{name}}} */ = {
                        isa = PBXGroup;
                        children = (
+                               0F4688401FDF8748004F34D4 /* assets */,
 """
                        # Reference all known files
                        for file in files do add """
@@ -328,6 +344,7 @@ class PbxprojectTemplate
 
                add """
                                0FDD07A21C6F8E0E006FF70E /* LaunchScreen.storyboard in Resources */,
+                               0F4688411FDF8748004F34D4 /* assets in Resources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
@@ -414,6 +431,7 @@ class PbxprojectTemplate
                                ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
                                INFOPLIST_FILE = {{{name}}}/Info.plist;
                                LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+                               OTHER_CFLAGS = "{{{cflags.escape_to_c}}}";
                                PRODUCT_NAME = "$(TARGET_NAME)";
                        };
                        name = Debug;
@@ -424,6 +442,7 @@ class PbxprojectTemplate
                                ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
                                INFOPLIST_FILE = {{{name}}}/Info.plist;
                                LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+                               OTHER_CFLAGS = "{{{cflags.escape_to_c}}}";
                                PRODUCT_NAME = "$(TARGET_NAME)";
                        };
                        name = Release;
index 766b1c2..00f601e 100644 (file)
@@ -7,3 +7,6 @@ emscripten
 ui_test
 readline
 postgres
+test_nitcorn
+test_annot_pkgconfig
+test_glsl_validation
index e69de29..a95d440 100644 (file)
@@ -0,0 +1 @@
+test_platform_ios
index c018d95..662f983 100644 (file)
@@ -7,4 +7,5 @@ out/test_platform_ios.bin/LaunchScreen.storyboardc/UIViewController-01J-lp-oVM.n
 out/test_platform_ios.bin/PkgInfo
 out/test_platform_ios.bin/_CodeSignature
 out/test_platform_ios.bin/_CodeSignature/CodeResources
+out/test_platform_ios.bin/assets
 out/test_platform_ios.bin/test_platform_ios
index 1cc4d27..b7ebf11 100755 (executable)
@@ -1,2 +1,2 @@
 #!/bin/sh
-./listfull.sh | xargs -E '' -x -- ./tests.sh "$@"
+./listfull.sh | xargs -E '' -- ./tests.sh "$@"
index 9587665..658cbbb 100755 (executable)
 # Set lang do default to avoid failed tests because of locale
 export LANG=C.UTF-8
 export LC_ALL=C.UTF-8
+if uname | grep Darwin 1>/dev/null 2>&1; then
+       export LANG=en_US.UTF-8
+       export LC_ALL=en_US.UTF-8
+fi
+
 export NIT_TESTING=true
 # Use the pid as a collision prevention
 export NIT_TESTING_ID=$$
@@ -29,12 +34,21 @@ unset NIT_DIR
 
 # Get the first Java lib available
 if which_java=$(which javac 2>/dev/null); then
-       JAVA_HOME=$(dirname $(dirname $(readlink -f "$which_java")))
+
+       if sh -c "readlink -f ." 1>/dev/null 2>&1; then
+               READLINK="readlink -f"
+       else
+               # Darwin?
+               READLINK="readlink"
+       fi
+       JAVA_HOME=$(dirname $(dirname $($READLINK "$which_java")))
 
        shopt -s nullglob
        paths=`echo $JAVA_HOME/jre/lib/*/{client,server}/libjvm.so`
-       paths=($paths)
-       JNI_LIB_PATH=`dirname ${paths[0]}`
+       if [ -n "$paths" ]; then
+               paths=($paths)
+               JNI_LIB_PATH=`dirname ${paths[0]}`
+       fi
        shopt -u nullglob
 fi