Use the same code for loading resources in all executables.

All of our executables need resources; e.g. the vector font is
a resource and it is necessary for generation. Before this commit,
the GUI executable loaded the resources in a nice way, and everything
else did it in a very ad-hoc, fragile way.

After this commit, all executables are placed in <build>/bin and
follow the same algorithm:
  * On Windows, resources are compiled and linked into every
    executable.
  * On Linux, resources are copied into <build>/res (which is
    tried first) and <prefix>/share/solvespace (which is tried
    second).
  * On macOS, resources are copied into <build>/res (which is
    tried first) and <build>/bin/solvespace.app/Contents/Resources
    (which is tried second).

In practice this means that we can add as many executables as we want
without duplicating lots of code. In addition, on macOS, we can
place supplementary executables into the bundle, and they can use
resources from the bundle transparently.
pull/36/merge
whitequark 2016-11-28 04:16:18 +00:00
parent 7681f6df74
commit 9301dec98d
17 changed files with 175 additions and 230 deletions

View File

@ -47,6 +47,9 @@ if(NOT WIN32 AND NOT APPLE)
set(GUI gtk2 CACHE STRING "GUI toolkit to use (one of: gtk2 gtk3)")
endif()
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
if(NOT CMAKE_C_COMPILER_ID STREQUAL CMAKE_CXX_COMPILER_ID)
message(FATAL_ERROR "C and C++ compilers should be supplied by the same vendor")
endif()

View File

@ -45,7 +45,7 @@ After that, build SolveSpace as following:
make
sudo make install
The application is built as `build/src/solvespace`.
The application is built as `build/bin/solvespace`.
A fully functional port to GTK3 is available, but not recommended
for use due to bugs in this toolkit.
@ -77,7 +77,7 @@ Or, build 64-bit SolveSpace as following:
-DENABLE_TESTS=OFF
make
The application is built as `build/src/solvespace.exe`.
The application is built as `build/bin/solvespace.exe`.
Space Navigator support will not be available.
@ -103,8 +103,8 @@ After that, build SolveSpace as following:
cmake .. -DENABLE_TESTS=OFF
make
The application is built in `build/src/solvespace.app`, and
the executable file is `build/src/solvespace.app/Contents/MacOS/solvespace`.
The application is built in `build/bin/solvespace.app`, and
the executable file is `build/bin/solvespace.app/Contents/MacOS/solvespace`.
[homebrew]: http://brew.sh/

View File

@ -11,15 +11,11 @@ build_script:
- msbuild "src\solvespace.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
- msbuild "test\solvespace_testsuite.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
test_script:
- test\%BUILD_TYPE%\solvespace_testsuite.exe
- bin\%BUILD_TYPE%\solvespace_testsuite.exe
artifacts:
- path: build\src\Debug\solvespace.exe
- path: build\bin\%BUILD_TYPE%\solvespace.exe
name: solvespace.exe
- path: build\src\Debug\solvespace.pdb
name: solvespace.pdb
- path: build\src\RelWithDebInfo\solvespace.exe
name: solvespace.exe
- path: build\src\RelWithDebInfo\solvespace.pdb
- path: build\bin\%BUILD_TYPE%\solvespace.pdb
name: solvespace.pdb
deploy:
# Releases to solvespace/solvespace

View File

@ -6,7 +6,11 @@ foreach(pkg_config_lib CAIRO)
endforeach()
add_executable(solvespace_benchmark
harness.cpp)
harness.cpp
$<TARGET_PROPERTY:resources,EXTRA_SOURCES>)
target_link_libraries(solvespace_benchmark
solvespace_headless)
add_dependencies(solvespace_benchmark
resources)

View File

@ -4,55 +4,6 @@
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include "solvespace.h"
#if defined(WIN32)
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace SolveSpace {
// These are defined in headless.cpp, and aren't exposed in solvespace.h.
extern std::string resourceDir;
}
static std::string ResourceRoot() {
static std::string rootDir;
if(!rootDir.empty()) return rootDir;
// No especially good way to do this, so let's assume somewhere up from
// the current directory there's our repository, with CMakeLists.txt, and
// pivot from there.
#if defined(WIN32)
wchar_t currentDirW[MAX_PATH];
GetCurrentDirectoryW(MAX_PATH, currentDirW);
rootDir = Narrow(currentDirW);
#else
rootDir = ".";
#endif
// We're never more than four levels deep.
for(size_t i = 0; i < 4; i++) {
std::string listsPath = rootDir;
listsPath += PATH_SEP;
listsPath += "CMakeLists.txt";
FILE *f = ssfopen(listsPath, "r");
if(f) {
fclose(f);
rootDir += PATH_SEP;
rootDir += "res";
return rootDir;
}
if(rootDir[0] == '.') {
rootDir += PATH_SEP;
rootDir += "..";
} else {
rootDir.erase(rootDir.rfind(PATH_SEP));
}
}
ssassert(false, "Couldn't locate repository root");
}
static bool RunBenchmark(std::function<void()> setupFn,
std::function<bool()> benchFn,
@ -90,14 +41,7 @@ static bool RunBenchmark(std::function<void()> setupFn,
}
int main(int argc, char **argv) {
#if defined(_MSC_VER)
_set_abort_behavior(0, _WRITE_ABORT_MSG);
#endif
#if defined(WIN32)
InitHeaps();
#endif
resourceDir = ResourceRoot();
InitPlatform();
std::string mode, filename;
if(argc == 3) {

View File

@ -30,18 +30,23 @@ if(WIN32)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${source}")
endfunction()
elseif(APPLE)
set(app_resource_dir ${CMAKE_BINARY_DIR}/src/solvespace.app/Contents/Resources)
set(app_resource_dir ${CMAKE_BINARY_DIR}/bin/solvespace.app/Contents/Resources)
set(cli_resource_dir ${CMAKE_BINARY_DIR}/res)
function(add_resource name)
set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name})
set(target ${app_resource_dir}/${name})
set(resource_list "${resource_list};${target}" PARENT_SCOPE)
set(target_app ${app_resource_dir}/${name})
set(target_cli ${cli_resource_dir}/${name})
set(resource_list "${resource_list};${target_app};${target_cli}" PARENT_SCOPE)
get_filename_component(target_dir ${target} DIRECTORY)
get_filename_component(target_app_dir ${target_app} DIRECTORY)
get_filename_component(target_cli_dir ${target_cli} DIRECTORY)
add_custom_command(
OUTPUT ${target}
COMMAND ${CMAKE_COMMAND} -E make_directory ${target_dir}
COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target}
OUTPUT ${target_app} ${target_cli}
COMMAND ${CMAKE_COMMAND} -E make_directory ${target_app_dir}
COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target_app}
COMMAND ${CMAKE_COMMAND} -E make_directory ${target_cli_dir}
COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target_cli}
COMMENT "Copying resource ${name}"
DEPENDS ${source}
VERBATIM)

View File

@ -20,6 +20,11 @@ else()
platform/unixutil.cpp)
endif()
if(APPLE)
set(util_LIBRARIES
${APPKIT_LIBRARY})
endif()
# libslvs
set(libslvs_SOURCES
@ -45,6 +50,9 @@ target_compile_definitions(slvs
target_include_directories(slvs
PUBLIC ${CMAKE_SOURCE_DIR}/include)
target_link_libraries(slvs
${util_LIBRARIES})
set_target_properties(slvs PROPERTIES
PUBLIC_HEADER ${CMAKE_SOURCE_DIR}/include/slvs.h
VERSION ${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR}
@ -101,9 +109,6 @@ elseif(APPLE)
set(platform_BUNDLED_LIBS
${PNG_LIBRARIES}
${FREETYPE_LIBRARIES})
set(platform_LIBRARIES
${APPKIT_LIBRARY})
elseif(HAVE_GTK2 OR HAVE_GTK3)
set(platform_SOURCES
platform/gtkmain.cpp
@ -192,6 +197,7 @@ add_library(solvespace_cad STATIC
target_link_libraries(solvespace_cad
dxfrw
${util_LIBRARIES}
${ZLIB_LIBRARY}
${PNG_LIBRARY}
${FREETYPE_LIBRARY}

View File

@ -81,7 +81,7 @@ void Slvs_MakeQuaternion(double ux, double uy, double uz,
void Slvs_Solve(Slvs_System *ssys, Slvs_hGroup shg)
{
if(!IsInit) {
InitHeaps();
InitPlatform();
IsInit = 1;
}

View File

@ -7,11 +7,7 @@
//-----------------------------------------------------------------------------
#include <mach/mach.h>
#include <mach/clock.h>
#import <AppKit/AppKit.h>
#include <iostream>
#include <map>
#import <AppKit/AppKit.h>
#include "solvespace.h"
@ -1138,26 +1134,6 @@ std::vector<std::string> SolveSpace::GetFontFiles() {
return fonts;
}
const void *SolveSpace::LoadResource(const std::string &name, size_t *size) {
static NSMutableDictionary *cache;
if(cache == nil) {
cache = [[NSMutableDictionary alloc] init];
}
NSString *key = [NSString stringWithUTF8String:name.c_str()];
NSData *data = [cache objectForKey:key];
if(data == nil) {
NSString *path = [[NSBundle mainBundle] pathForResource:key ofType:nil];
data = [[NSFileHandle fileHandleForReadingAtPath:path] readDataToEndOfFile];
ssassert(data != nil, "Cannot find resource");
[cache setObject:data forKey:key];
}
*size = [data length];
return [data bytes];
}
/* Application lifecycle */
@interface ApplicationDelegate : NSObject<NSApplicationDelegate>

View File

@ -1482,73 +1482,6 @@ std::vector<std::string> GetFontFiles() {
return fonts;
}
std::string ExpandPath(std::string path) {
char *expanded_c_path = realpath(path.c_str(), NULL);
if(expanded_c_path == NULL) {
fprintf(stderr, "realpath(%s): %s\n", path.c_str(), strerror(errno));
return "";
}
std::string expanded_path = expanded_c_path;
free(expanded_c_path);
return expanded_path;
}
static std::string resource_dir;
void FindLocalResourceDir(const char *argv0) {
// Getting path to your own executable is a total portability disaster.
// Good job *nix OSes; you're basically all awful here.
std::string self_path;
#if defined(__linux__)
self_path = "/proc/self/exe";
#elif defined(__NetBSD__)
self_path = "/proc/curproc/exe"
#elif defined(__OpenBSD__)
self_path = "/proc/curproc/file";
#else
self_path = argv0;
#endif
resource_dir = ExpandPath(self_path);
if(resource_dir.empty()) {
fprintf(stderr, "Cannot determine path to executable; using global resources.\n");
return;
}
resource_dir.erase(resource_dir.rfind('/'));
resource_dir += "/../res";
resource_dir = ExpandPath(resource_dir);
}
const void *LoadResource(const std::string &name, size_t *size) {
static std::map<std::string, std::vector<uint8_t>> cache;
auto it = cache.find(name);
if(it == cache.end()) {
struct stat st;
std::string path;
if(resource_dir.empty()) {
path = (UNIX_DATADIR "/") + name;
} else {
path = resource_dir + "/" + name;
}
if(stat(path.c_str(), &st)) {
ssassert(!stat(path.c_str(), &st), "Cannot find resource");
}
std::vector<uint8_t> data(st.st_size);
FILE *f = ssfopen(path.c_str(), "rb");
ssassert(f != NULL, "Cannot open resource");
fread(&data[0], 1, st.st_size, f);
fclose(f);
cache.emplace(name, std::move(data));
it = cache.find(name);
}
*size = (*it).second.size();
return &(*it).second[0];
}
/* Space Navigator support */
@ -1612,9 +1545,6 @@ int main(int argc, char** argv) {
ambiguous. */
gtk_disable_setlocale();
/* If we're running from the build directory, grab the local resources. */
FindLocalResourceDir(argv[0]);
Gtk::Main main(argc, argv);
#ifdef HAVE_SPACEWARE

View File

@ -251,31 +251,6 @@ std::vector<std::string> GetFontFiles() {
return fontFiles;
}
std::string resourceDir;
const void *LoadResource(const std::string &name, size_t *size) {
static std::map<std::string, std::vector<uint8_t>> cache;
auto it = cache.find(name);
if(it == cache.end()) {
std::string path = resourceDir + "/" + name;
std::vector<uint8_t> data;
FILE *f = ssfopen(PathSepUnixToPlatform(path).c_str(), "rb");
ssassert(f != NULL, "Cannot open resource");
fseek(f, 0, SEEK_END);
data.resize(ftell(f));
fseek(f, 0, SEEK_SET);
fread(&data[0], 1, data.size(), f);
fclose(f);
cache.emplace(name, std::move(data));
it = cache.find(name);
}
*size = (*it).second.size();
return &(*it).second[0];
}
//-----------------------------------------------------------------------------
// Application lifecycle
//-----------------------------------------------------------------------------

View File

@ -7,13 +7,18 @@
// Copyright 2008-2013 Jonathan Westhues.
// Copyright 2013 Daniel Richard G. <skunk@iSKUNK.ORG>
//-----------------------------------------------------------------------------
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <execinfo.h>
#ifdef __APPLE__
# include <strings.h> // for strcasecmp
# include <CoreFoundation/CFString.h>
# include <CoreFoundation/CFURL.h>
# include <CoreFoundation/CFBundle.h>
#endif
#include "solvespace.h"
#include "config.h"
namespace SolveSpace {
@ -89,6 +94,104 @@ void ssremove(const std::string &filename)
remove(filename.c_str());
}
static std::string ExpandPath(std::string path) {
char *expanded_c_path = realpath(path.c_str(), NULL);
if(expanded_c_path == NULL) {
fprintf(stderr, "realpath(%s): %s\n", path.c_str(), strerror(errno));
return "";
}
std::string expanded_path = expanded_c_path;
free(expanded_c_path);
return expanded_path;
}
static const std::string &FindLocalResourceDir() {
static std::string resourceDir;
static bool checked;
if(checked) return resourceDir;
checked = true;
// Getting path to your own executable is a total portability disaster.
// Good job *nix OSes; you're basically all awful here.
std::string selfPath;
#if defined(__linux__)
selfPath = "/proc/self/exe";
#elif defined(__NetBSD__)
selfPath = "/proc/curproc/exe"
#elif defined(__OpenBSD__)
selfPath = "/proc/curproc/file";
#elif defined(__APPLE__)
CFURLRef cfUrl =
CFBundleCopyExecutableURL(CFBundleGetMainBundle());
CFStringRef cfPath = CFURLCopyFileSystemPath(cfUrl, kCFURLPOSIXPathStyle);
selfPath.resize(CFStringGetLength(cfPath) + 1); // reserve space for NUL
ssassert(CFStringGetCString(cfPath, &selfPath[0], selfPath.size(), kCFStringEncodingUTF8),
"Cannot convert CFString to C string");
selfPath.resize(selfPath.size() - 1);
CFRelease(cfUrl);
CFRelease(cfPath);
#else
// We don't know how to find the local resource directory on this platform,
// so use the global one (by returning an empty string).
return resourceDir;
#endif
resourceDir = ExpandPath(selfPath);
if(!resourceDir.empty()) {
resourceDir.erase(resourceDir.rfind('/'));
resourceDir += "/../res";
resourceDir = ExpandPath(resourceDir);
}
if(!resourceDir.empty()) {
struct stat st;
if(stat(resourceDir.c_str(), &st)) {
// We looked at the path where the local resource directory ought to be,
// but there isn't one, so use the global one.
resourceDir = "";
}
}
return resourceDir;
}
const void *LoadResource(const std::string &name, size_t *size) {
static std::map<std::string, std::string> cache;
auto it = cache.find(name);
if(it == cache.end()) {
const std::string &resourceDir = FindLocalResourceDir();
std::string path;
if(resourceDir.empty()) {
#if defined(__APPLE__)
CFStringRef cfName =
CFStringCreateWithCString(kCFAllocatorDefault, name.c_str(),
kCFStringEncodingUTF8);
CFURLRef cfUrl =
CFBundleCopyResourceURL(CFBundleGetMainBundle(), cfName, NULL, NULL);
CFStringRef cfPath = CFURLCopyFileSystemPath(cfUrl, kCFURLPOSIXPathStyle);
path.resize(CFStringGetLength(cfPath) + 1); // reserve space for NUL
ssassert(CFStringGetCString(cfPath, &path[0], path.size(), kCFStringEncodingUTF8),
"Cannot convert CFString to C string");
path.resize(path.size() - 1);
CFRelease(cfName);
CFRelease(cfUrl);
CFRelease(cfPath);
#else
path = (UNIX_DATADIR "/") + name;
#endif
} else {
path = resourceDir + "/" + name;
}
ssassert(ReadFile(path, &cache[name]), "Cannot read resource");
it = cache.find(name);
}
*size = (*it).second.size();
return static_cast<const void *>(&(*it).second[0]);
}
//-----------------------------------------------------------------------------
// A separate heap, on which we allocate expressions. Maybe a bit faster,
// since fragmentation is less of a concern, and it also makes it possible
@ -150,7 +253,7 @@ void MemFree(void *p) {
free(p);
}
void InitHeaps(void) {
void InitPlatform(void) {
/* nothing to do */
}

View File

@ -1397,16 +1397,6 @@ static void CreateMainWindows()
ClientIsSmallerBy = (r.bottom - r.top) - (rc.bottom - rc.top);
}
const void *SolveSpace::LoadResource(const std::string &name, size_t *size) {
HRSRC hres = FindResourceW(NULL, Widen(name).c_str(), RT_RCDATA);
ssassert(hres != NULL, "Cannot find resource");
HGLOBAL res = LoadResource(NULL, hres);
ssassert(res != NULL, "Cannot load resource");
*size = SizeofResource(NULL, hres);
return LockResource(res);
}
#ifdef HAVE_SPACEWARE
//-----------------------------------------------------------------------------
// Test if a message comes from the SpaceNavigator device. If yes, dispatch
@ -1474,7 +1464,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
ShowWindow(GraphicsWnd, SW_SHOW);
// Create the heaps for all dynamic memory (AllocTemporary, MemAlloc)
InitHeaps();
InitPlatform();
// Pull out the Unicode command line.
int argc;

View File

@ -144,6 +144,16 @@ void ssremove(const std::string &filename)
_wremove(Widen(filename).c_str());
}
const void *LoadResource(const std::string &name, size_t *size) {
HRSRC hres = FindResourceW(NULL, Widen(name).c_str(), RT_RCDATA);
ssassert(hres != NULL, "Cannot find resource");
HGLOBAL res = ::LoadResource(NULL, hres);
ssassert(res != NULL, "Cannot load resource");
*size = SizeofResource(NULL, hres);
return LockResource(res);
}
//-----------------------------------------------------------------------------
// A separate heap, on which we allocate expressions. Maybe a bit faster,
// since no fragmentation issues whatsoever, and it also makes it possible
@ -182,10 +192,17 @@ void vl() {
ssassert(HeapValidate(PermHeap, HEAP_NO_SERIALIZE, NULL), "Corrupted heap");
}
void InitHeaps() {
void InitPlatform() {
// Create the heap used for long-lived stuff (that gets freed piecewise).
PermHeap = HeapCreate(HEAP_NO_SERIALIZE, 1024*1024*20, 0);
// Create the heap that we use to store Exprs and other temp stuff.
FreeAllTemporary();
#if !defined(LIBRARY) && defined(_MSC_VER)
// Don't display the abort message; it is aggravating in CLI binaries
// and results in infinite WndProc recursion in GUI binaries.
_set_abort_behavior(0, _WRITE_ABORT_MSG);
#endif
}
}

View File

@ -294,7 +294,7 @@ void FreeTemporary(void *p);
void FreeAllTemporary();
void *MemAlloc(size_t n);
void MemFree(void *p);
void InitHeaps();
void InitPlatform();
void vl(); // debug function to validate heaps
#include "resource.h"

View File

@ -58,12 +58,16 @@ set(testsuite_SOURCES
)
add_executable(solvespace_testsuite
${testsuite_SOURCES})
${testsuite_SOURCES}
$<TARGET_PROPERTY:resources,EXTRA_SOURCES>)
target_link_libraries(solvespace_testsuite
solvespace_headless
${COVERAGE_LIBRARY})
add_dependencies(solvespace_testsuite
resources)
add_custom_target(solvespace-test ALL
COMMAND $<TARGET_FILE:solvespace_testsuite>
COMMENT "Testing SolveSpace"

View File

@ -3,18 +3,19 @@
//
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include "harness.h"
#include <regex>
#include <cairo.h>
#include "harness.h"
#if defined(WIN32)
#include <windows.h>
# include <windows.h>
#else
#include <unistd.h>
# include <unistd.h>
#endif
namespace SolveSpace {
// These are defined in headless.cpp, and aren't exposed in solvespace.h.
extern std::string resourceDir;
extern std::vector<std::string> fontFiles;
extern bool antialias;
extern std::shared_ptr<Pixmap> framebuffer;
@ -291,12 +292,7 @@ int Test::Case::Register(Test::Case testCase) {
}
int main(int argc, char **argv) {
#if defined(_MSC_VER)
_set_abort_behavior(0, _WRITE_ABORT_MSG);
#endif
#if defined(WIN32)
InitHeaps();
#endif
InitPlatform();
std::regex filter(".*");
if(argc == 1) {
@ -307,10 +303,6 @@ int main(int argc, char **argv) {
return 1;
}
resourceDir = HostRoot();
resourceDir.erase(resourceDir.rfind(HOST_PATH_SEP) + 1);
resourceDir += "res";
fontFiles.push_back(HostRoot() + HOST_PATH_SEP + "Gentium-R.ttf");
// Different Cairo versions have different antialiasing algorithms.