├── .github └── workflows │ └── build.yml ├── .gitignore ├── CMakeLists.txt ├── ENCRYPTION_KEY ├── LICENSE ├── README.md ├── app └── index.html ├── docs ├── architecture.md ├── custom_protocol.md └── native_bindings.md ├── package.json ├── scripts ├── asar.js ├── bin2c.js ├── bootstrap.js ├── build.js ├── common.js ├── dist.js ├── download_libyue.js └── run.js └── src ├── exe.ico ├── exe.manifest ├── exe.rc └── main.cc /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: >- 8 | ${{ 9 | (matrix.targetOs == 'mac' && matrix.targetArch == 'x64') && 10 | 'macos-13' || 11 | (fromJson('{"linux":"ubuntu-22.04","mac":"macos-14","win":"windows-2022"}')[matrix.targetOs]) 12 | }} 13 | continue-on-error: false 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | targetOs: [linux, mac, win] 19 | targetArch: [x64] 20 | include: 21 | - targetOs: mac 22 | targetArch: arm64 23 | - targetOs: win 24 | targetArch: x86 25 | 26 | steps: 27 | - name: Install Linux Dependencies 28 | if: matrix.targetOs == 'linux' 29 | run: | 30 | sudo apt update 31 | sudo apt install -y cmake libgtk-3-dev libnotify-dev libwebkit2gtk-4.0-dev 32 | sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang 100 33 | sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang 100 34 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 35 | 36 | # Work around CI bug https://github.com/actions/runner-images/issues/8659. 37 | - uses: mjp41/workaround8649@c8550b715ccdc17f89c8d5c28d7a48eeff9c94a8 38 | if: matrix.targetOs == 'linux' && matrix.targetArch == 'x64' 39 | 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Build 44 | run: yarn && yarn dist 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.zip 3 | 4 | yarn.lock 5 | package-lock.json 6 | npm-debug.log 7 | node_modules 8 | 9 | /libyue 10 | /out 11 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | # Use Windows 10 SDK. 4 | if(WIN32) 5 | set(CMAKE_SYSTEM_VERSION 10.0) 6 | endif() 7 | 8 | # Project name and executable name. 9 | project(boilerplate) 10 | set(APP_NAME "boilerplate") 11 | 12 | # The main executable. 13 | file(GLOB SRC_FILES "src/*.h" "src/*.cc") 14 | add_executable(${APP_NAME} 15 | ${SRC_FILES} "${CMAKE_CURRENT_BINARY_DIR}/encryption_key.h") 16 | if(WIN32) 17 | target_sources(${APP_NAME} PRIVATE "src/exe.rc" "src/exe.manifest") 18 | endif() 19 | 20 | # Link with Yue. 21 | add_subdirectory(libyue) 22 | target_link_libraries(${APP_NAME} Yue) 23 | 24 | # Convert the ENCRYPTION_KEY file into encryption_key.h 25 | add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/encryption_key.h" 26 | COMMAND node scripts/bin2c.js ENCRYPTION_KEY "${CMAKE_CURRENT_BINARY_DIR}/encryption_key.h" 27 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 28 | DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/ENCRYPTION_KEY") 29 | target_include_directories(${APP_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") 30 | 31 | # Package the app into asar format. 32 | file(GLOB APP_FILES "app/[_a-z]*") 33 | add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/app.ear" 34 | COMMAND node scripts/asar.js app "${CMAKE_CURRENT_BINARY_DIR}/app.ear" 35 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 36 | DEPENDS ${APP_FILES} "${CMAKE_CURRENT_SOURCE_DIR}/ENCRYPTION_KEY") 37 | add_custom_target(ASAR ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/app.ear") 38 | add_dependencies(${APP_NAME} ASAR) 39 | -------------------------------------------------------------------------------- /ENCRYPTION_KEY: -------------------------------------------------------------------------------- 1 | USE YOUR OWN KEY -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boilerplate-cpp 2 | 3 | A C++ template of the [Yue library](https://github.com/yue/yue) for building 4 | cross-platform desktop apps with system webview and native GUI widgets. 5 | 6 | ## Features 7 | 8 | * Quickly build desktop apps with HTML and JavaScript. 9 | * Generate single-file executables for macOS/Linux/Windows. 10 | * Source code encryptions. 11 | * Use system webviews to minimize binary size. 12 | * Use native GUI widgets to extend the UI. 13 | * Extremely easy to add C++ native bindings to web pages. 14 | * Custom protocols in webview. 15 | 16 | ## How to use 17 | 18 | 1. Clone the project 19 | 2. Put your web app in `app/` 20 | 3. `npm install` to build 21 | 4. `npm start` to start the app 22 | 5. `npm run dist` to create distribution 23 | 24 | ## Screenshots (in Debug mode) 25 | 26 | | macOS | Linux | Windows | 27 | | ----------------- | ----------------- | ----------------- | 28 | | ![][mac-browser] | ![][linux-browser] | ![][win-browser] | 29 | 30 | ## Docs 31 | 32 | * [Architecture](https://github.com/yue/boilerplate-cpp/blob/master/docs/architecture.md) 33 | * [Add native bindings to web pages](https://github.com/yue/boilerplate-cpp/blob/master/docs/native_bindings.md) 34 | * [Custom protocol](https://github.com/yue/boilerplate-cpp/blob/master/docs/custom_protocol.md) 35 | * [Yue documents (external link)](http://libyue.com/docs/latest/cpp/) 36 | 37 | ## License 38 | 39 | Public domain. 40 | 41 | ## Contributions 42 | 43 | Since this project mostly serves as a template with minimal features, pull 44 | requests to add new features might be rejected. 45 | 46 | ## Limitations 47 | 48 | Please consider following limitations before writing your app with Yue, some of 49 | them are impossible to solve even in future versions. 50 | 51 | ### Webview 52 | 53 | Due to using system webview, there is no way to choose browser engine or browser 54 | version, so we can not control how web page is rendered or which HTML5 features 55 | are available, thus the pain of browser compatibility. 56 | 57 | The good news is, because Yue uses `webkit2gtk` on Linux and `WKWebView` on 58 | macOS, you can at least expect modern web engines on these two platforms. 59 | 60 | The APIs provided in Yue are also highly limited to what system APIs have, and 61 | we can only provide APIs based on existing system APIs. 62 | 63 | ### Source code protection 64 | 65 | While the source code of your app is encrypted with a secure algorithm, there is 66 | really nothing we can do to prevent a determined hacker to get the source code 67 | from the executable files. 68 | 69 | ### Accessibility 70 | 71 | On macOS and Linux, widgets of Yue are just wrappers of existing native widgets, 72 | so you have the same level of accessibility with system toolkits. 73 | 74 | However on Windows certain widgets (for example buttons) are windowless and do 75 | not have accessibility implemented yet, while it is of high priority on my TODO 76 | list, please only use the widgets with accessibility implemented (for example 77 | webview and text editor) if this is important to you. 78 | 79 | [mac-browser]: https://user-images.githubusercontent.com/639601/36703168-bcaedb8e-1b9d-11e8-8260-89eef8157adc.png 80 | [win-browser]: https://user-images.githubusercontent.com/639601/36703170-bd14f70c-1b9d-11e8-91d8-86664431970c.png 81 | [linux-browser]: https://user-images.githubusercontent.com/639601/36703169-bceaeafc-1b9d-11e8-9cd4-70a030d9ebe5.png 82 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CPU Info 6 | 32 | 33 | 34 |
35 |
Show CPU Info
36 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture of boilerplate-cpp 2 | 3 | This project uses CMake for building, and uses Node.js as build scripts. 4 | 5 | ## Source code structure 6 | 7 | * `app/` - The web app. 8 | * `src/` - The C++ code for creating window and loading the web app. 9 | * `libyue/` - Downloaded static libraries of `Yue`. 10 | * `node_modules/` - Third party Node.js modules installed by `npm`. 11 | * `scripts/` - Scripts for building the project. 12 | * `out/` - Store generated binary files. 13 | * `CMakeLists.txt` - The CMake configuration for buliding the project. 14 | * `ENCRYPTION_KEY` - The 16 bytes key used for encrypting the web app. 15 | 16 | ## System requirements 17 | 18 | The Yue library requires C++20 for building, so newer toolchains are required. 19 | 20 | * Linux: 21 | * GCC >=10 or clang 22 | * libstdc++6 or newer 23 | * libwebkit2gtk >= 2.8 24 | * Node.js >= 20 25 | * macOS: 26 | * Xcode >= 15 27 | * macOS SDK >= 11 28 | * Node.js >= 20 29 | * Windows: 30 | * Visual Studio 2022 with Windows 10 SDK 31 | * An x64 machine with Node.js x64 >= 20 installed. 32 | 33 | Note that on Linux due to using libstdc++6 and libwebkit2gtk 2.8, the generated 34 | binary can only run on newer distributions, e.g. at least Ubuntu 16.04 and 35 | Debian Stretch. 36 | 37 | On macOS due to using the `WKWebView` API, only macOS 11.0 and later are 38 | supported. 39 | 40 | On Windows currently Windows Vista and later are supported. It should be 41 | possible to support Windows XP with some efforts, but it is not on our roadmap. 42 | 43 | ## Building 44 | 45 | While it is possible to only use `cmake` to build this project, there are `npm` 46 | scripts defined to make the buliding easier. 47 | 48 | ### `npm run build` 49 | 50 | Calls `scripts/build.js` to build Debug version. 51 | 52 | ### `npm run dist` 53 | 54 | Calls `scripts/dist.js` to build the Release version and archives the generated 55 | binary into a zip file. 56 | 57 | ### `npm install` 58 | 59 | Installs all dependencies and build Debug version. 60 | 61 | ### `npm start` 62 | 63 | Calls `scripts/run.js` to start the Debug version. 64 | 65 | ## Debugging 66 | 67 | By default the app is built in Debug mode, which has devtools and builtin 68 | context menu enabled, with a toolbar on the top of the webview. The debugging 69 | features are removed when buliding Release version with `npm run dist`. 70 | 71 | Note that devtools are currently only available on Linux and macOS. 72 | 73 | ## IDE support 74 | 75 | With the power of `cmake`, on macOS a Xcode project will be generated, and on 76 | Windows a Visual Studio 2022 solution will be generated. So you can develop your 77 | application with the IDEs on macOS and Windows. 78 | 79 | Also note that due to limitations of `cmake`, we can only build for one CPU 80 | architecture per one IDE solution, it is not possible to build for both `x86` 81 | and `x64` within the same IDE solution. 82 | 83 | ## Cross compilation 84 | 85 | I haven't figured out the proper way of doing cross compilation on Linux with 86 | `cmake`, so on Linux currently you can only build for `x86` on a `x86` machine, 87 | and so are the other architectures. 88 | 89 | For Windows you can set the `npm_config_arch` environment variable to specify 90 | the architecture to build for (`x64` or `ia32`), if not set the target 91 | architecture would be determined by the value of `process.arch` from Node.js. 92 | 93 | ## Packaging 94 | 95 | In order to generate a single-file executable, the `app/` files are archived 96 | using [asar](https://github.com/electron/asar) format, and then concatenated 97 | to the final executable file. 98 | 99 | To make it hard to get the source code of the web app, the `app/` files are 100 | encrypted with AES. When developing your own app, you should change the content 101 | of `ENCRYPTION_KEY` to your own key. 102 | 103 | The packaging part is done in the `scripts/asar.js` script. 104 | -------------------------------------------------------------------------------- /docs/custom_protocol.md: -------------------------------------------------------------------------------- 1 | # Custom protocol 2 | 3 | To load the web app from `asar` archive, custom protocols are used instead of 4 | the `file://` protocol. 5 | 6 | ## Defining custom protocol 7 | 8 | In Yue custom protocols are defined with the `nu::Browser::RegisterProtocol` 9 | API, which should be called with the scheme's name and a handler. 10 | 11 | The handler is called whenever a request with the registered custom scheme is 12 | initalized, and the handler should return an instance of the `nu::ProtocolJob` 13 | class. 14 | 15 | ## Protocol jobs 16 | 17 | The `nu::ProtocolJob` class is a pure virtual class that can not be created 18 | directly, you must create its sub-classes instead. Currently following types of 19 | protocol job can be used: 20 | 21 | * `ProtocolStringJob` - Use string as response. 22 | * `ProtocolFileJob` - Read a file as response. 23 | * `ProtocolAsarJob` - Read a file from `asar` archive as response. 24 | 25 | It is also possible to declare a custom class that derives from `ProtocolJob`, 26 | but it is not recommended for now since the whole API is experimental and will 27 | change in future. 28 | -------------------------------------------------------------------------------- /docs/native_bindings.md: -------------------------------------------------------------------------------- 1 | # Add native bindings to web pages 2 | 3 | Due to limitations of system webview APIs, we are unable to provide direct 4 | access to web pages' DOM trees, but it is still possible to provide a way to 5 | invoke native code from web pages, and post messages from native code to web 6 | pages. 7 | 8 | ## Native bindings 9 | 10 | By calling the `nu::Browser::AddBinding` API, you can bind arbitrary C++ 11 | function to web pages as long as the C++ parameters can be converted from 12 | JavaScript automatically. 13 | 14 | For example adding a `window.storeIntRecord` binding: 15 | 16 | ```c++ 17 | void StoreIntRecord(const std::string& key, int value) { 18 | LOG(ERROR) << "Adding record: " << key << " " << value; 19 | } 20 | 21 | browser->AddBinding("storeIntRecord", &StoreIntRecord); 22 | ``` 23 | 24 | Under the hood, each JavaScript argument is converted to [`base::Value`][value] 25 | first, and then converted to the C++ types defined in the binding's prototype. 26 | So the parameters of the binding must be convertable from the 27 | [`base::Value`][value], otherwise Yue would not be able to do automatic 28 | conversion and compilation error would happen. 29 | 30 | If you would like to use a custom type as binding's parameter, you can use 31 | [`base::Value`][value] as parameter directly: 32 | 33 | ```c++ 34 | void StoreRecord(const std::string& key, base::Value value) { 35 | LOG(ERROR) << "Adding record: " << key << " " << value; 36 | } 37 | 38 | browser->AddBinding("storeRecord", &StoreRecord); 39 | ``` 40 | 41 | You can also use `nu::Browser*` as a parameter, which would be assigned with 42 | a pointer to the browser calling the binding, and it would not interrupt the 43 | automatic conversion of other parameters. 44 | 45 | ## Binding name 46 | 47 | By default the native bindings are added to the `window` object, by calling the 48 | `nu::Browser::SetBindingName(name)` API, you can make Yue dd the bindings to 49 | the `window[name]` object instead. 50 | 51 | ## Native bindings are called asynchronously 52 | 53 | Due to the multi-processes architecture used by WebKit2 engine, the native 54 | bindings are not called from web pages in the same process. Whenever you invoke 55 | native binding from the web page, internally an inter-process-message will be 56 | sent and the native function will be called asynchronously. 57 | 58 | So it is impossible for native bindings to return values to web pages, the only 59 | way for communication is to send a message back by calling the 60 | `nu::Browser::ExecuteJavaScript` API: 61 | 62 | ```c++ 63 | void Ping(nu::Browser* browser) { 64 | browser->ExecuteJavaScript("window.pong()", nullptr /* callback */); 65 | } 66 | 67 | browser->AddBinding("ping", &Ping); 68 | ``` 69 | 70 | 71 | ```js 72 | window.pong = function() { 73 | console.log('pong') 74 | } 75 | window.ping() 76 | ``` 77 | 78 | ## Lambda functions 79 | 80 | It is also possible to pass a C++ lambda function as native binding: 81 | 82 | ```c++ 83 | browser->AddBinding("lambda", [](base::Value arg) { 84 | LOG(ERROR) << arg; 85 | }); 86 | ``` 87 | 88 | But note that the lambdas must have NO captures, because we have to deduce the 89 | type of arguments at compile-time, and we are unable to do so for lambdas with 90 | captures. 91 | 92 | ## Handle arbitrary arguments 93 | 94 | The `AddBinding` API can automatically convert arguments to C++ types, however 95 | if you want to handle the arguments yourself, you can use the 96 | `nu::Browser::AddRawBinding` API to set a binding that accepts a single 97 | [`base::Value`][value] argument, which is a list converted from JavaScript's 98 | `arguments` object: 99 | 100 | ```c++ 101 | browser->AddRawBinding("long", [](nu::Browser*, base::Value args) { 102 | LOG(ERROR) << "Length of arguments: " << args.GetList().size(); 103 | }); 104 | ``` 105 | 106 | [value]: https://cs.chromium.org/chromium/src/base/values.h?sq=package:chromium 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "Build desktop apps with system webview and native controls", 6 | "scripts": { 7 | "start": "node scripts/run.js", 8 | "install": "npm run download && npm run bootstrap && npm run build", 9 | "download": "node scripts/download_libyue.js v0.15.2", 10 | "bootstrap": "node scripts/bootstrap.js", 11 | "build": "node scripts/build.js", 12 | "dist": "node scripts/dist.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/yue/boilerplate-cpp.git" 17 | }, 18 | "license": "public domain", 19 | "devDependencies": { 20 | "@electron/asar": "3.2.9", 21 | "@indutny/yazl": "2.7.0", 22 | "@yogalayout/cmake-bin": "3.28.0-1", 23 | "download-yue": "2.x" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scripts/asar.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const crypto = require('crypto') 4 | const fs = require('fs') 5 | const asar = require('@electron/asar') 6 | 7 | require('./common') 8 | 9 | const key = fs.readFileSync('ENCRYPTION_KEY') 10 | const iv = 'yue is good lib!' 11 | 12 | if (process.argv.length != 4) { 13 | console.error('Usage: asar.js source target') 14 | process.exit(1) 15 | } 16 | 17 | const source = process.argv[2] 18 | const target = process.argv[3] 19 | 20 | function transform() { 21 | const cipher = crypto.createCipheriv('aes-128-cbc', key, iv) 22 | cipher.setAutoPadding(true) 23 | return cipher 24 | } 25 | 26 | function appendMeta() { 27 | const stat = fs.statSync(target) 28 | const meta = Buffer.alloc(8 + 1 + 4) 29 | const asarSize = stat.size + meta.length 30 | meta.writeDoubleLE(asarSize, 0) 31 | meta.writeUInt8(2, 8) 32 | meta.write('ASAR', 9) 33 | fs.appendFileSync(target, meta) 34 | } 35 | 36 | asar.createPackageWithOptions(source, target, { transform }).then(appendMeta) 37 | -------------------------------------------------------------------------------- /scripts/bin2c.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | if (process.argv.length != 4) { 7 | console.error('Usage: bin2c.js source target') 8 | process.exit(1) 9 | } 10 | 11 | const source = process.argv[2] 12 | const target = process.argv[3] 13 | 14 | const hexChar = ['0', '1', '2', '3', '4', '5', '6', '7','8', '9', 'A', 'B', 'C', 'D', 'E', 'F'] 15 | 16 | const bin = fs.readFileSync(source) 17 | let c = `static const char ${path.basename(source)}[] = {\n` 18 | for (let i = 0; i < bin.length; ++i) { 19 | const b = bin[i] 20 | c += '0x' + hexChar[(b >> 4) & 0x0f] + hexChar[b & 0x0f] 21 | if ((i + 1) % 8 == 0) 22 | c += ',\n' 23 | else 24 | c += ', ' 25 | } 26 | c += '};' 27 | 28 | fs.writeFileSync(target, c); 29 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path') 4 | 5 | const {targetCpu, mkdir, spawnSync} = require('./common') 6 | 7 | const cmake = path.resolve('node_modules', '@yogalayout', 'cmake-bin', 'bin', 'cmake') 8 | 9 | mkdir('out') 10 | 11 | if (process.platform == 'win32') { 12 | process.exit(spawnSync(process.execPath, 13 | [cmake, '-S', '.', '-B', 'out', 14 | '-G', 'Visual Studio 17 2022', 15 | '-A', targetCpu == 'x64' ? 'x64' : 'Win32']).status) 16 | } else { 17 | mkdir('out/Release') 18 | let code = spawnSync(process.execPath, 19 | [cmake, '-D', `CMAKE_BUILD_TYPE=Release`, '../..'], 20 | {cwd: 'out/Release'}).status 21 | if (code != 0) 22 | process.exit(code) 23 | mkdir('out/Debug') 24 | process.exit(spawnSync(process.execPath, 25 | [cmake, '-D', `CMAKE_BUILD_TYPE=Debug`, '../..'], 26 | {cwd: 'out/Debug'}).status) 27 | } 28 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {targetCpu, spawnSync} = require('./common') 4 | 5 | const os = require('os') 6 | const path = require('path') 7 | 8 | const name = require('../package.json').name 9 | const config = process.argv[2] ? process.argv[2] : 'Debug' 10 | 11 | if (process.platform == 'win32') { 12 | const vsPaths = [ 13 | 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin', 14 | 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\MSBuild\\Current\\Bin', 15 | process.env.PATH 16 | ] 17 | const env = Object.assign(process.env, {PATH: vsPaths.join(path.delimiter)}) 18 | const platform = targetCpu == 'x64' ? 'x64' : 'Win32' 19 | process.exit(spawnSync( 20 | 'msbuild', 21 | [name + '.sln', 22 | '/m:' + os.cpus().length, 23 | '/p:Configuration=' + config, 24 | '/p:Platform=' + platform], 25 | {cwd: 'out', env}).status) 26 | } else { 27 | process.exit(spawnSync( 28 | 'make', 29 | ['-j', os.cpus().length], 30 | {cwd: `out/${config}`}).status) 31 | } 32 | -------------------------------------------------------------------------------- /scripts/common.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const https = require('https') 4 | const {execSync, spawnSync} = require('child_process') 5 | 6 | // Switch to root dir. 7 | process.chdir(path.dirname(__dirname)) 8 | 9 | // The target cpu. 10 | const narch = process.env.npm_config_arch ? process.env.npm_config_arch 11 | : process.arch 12 | const targetCpu = { 13 | x64: 'x64', 14 | ia32: 'x86', 15 | arm: 'arm', 16 | arm64: 'arm64', 17 | }[narch] 18 | 19 | // Make dir and ignore error. 20 | function mkdir(dir) { 21 | if (fs.existsSync(dir)) return 22 | mkdir(path.dirname(dir)) 23 | fs.mkdirSync(dir) 24 | } 25 | 26 | // Helper around execSync. 27 | const execSyncWrapper = (command, options = {}) => { 28 | // Print command output by default. 29 | if (!options.stdio) 30 | options.stdio = 'inherit' 31 | // Merge the custom env to global env. 32 | if (options.env) 33 | options.env = Object.assign(options.env, process.env) 34 | return execSync(command, options) 35 | } 36 | 37 | const spawnSyncWrapper = (exec, args, options = {}) => { 38 | // Print command output by default. 39 | if (!options.stdio) 40 | options.stdio = 'inherit' 41 | // Merge the custom env to global env. 42 | if (options.env) 43 | options.env = Object.assign(options.env, process.env) 44 | return spawnSync(exec, args, options) 45 | } 46 | 47 | // Export public APIs. 48 | module.exports = { 49 | targetCpu, 50 | mkdir, 51 | execSync: execSyncWrapper, 52 | spawnSync: spawnSyncWrapper, 53 | } 54 | -------------------------------------------------------------------------------- /scripts/dist.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const yazl = require('@indutny/yazl') 6 | 7 | const {targetCpu, execSync} = require('./common') 8 | 9 | const {name, version} = require('../package.json') 10 | 11 | // Release build. 12 | execSync('node scripts/build.js Release') 13 | 14 | // Path to generated exe. 15 | const outpath = process.platform === 'win32' ? 'out' : 'out/Release' 16 | const exepath = process.platform === 'win32' ? `${outpath}/Release/${name}.exe` 17 | : `${outpath}/${name}` 18 | 19 | // Concat the exe and app.ear. 20 | fs.appendFileSync(exepath, fs.readFileSync(`${outpath}/app.ear`)) 21 | 22 | // Create zip. 23 | const zip = new yazl.ZipFile 24 | zip.addFile('libyue/LICENSE', 'LICENSE') 25 | zip.addFile(exepath, path.basename(exepath), { mode: parseInt("0100755", 8) }) 26 | zip.outputStream.pipe(fs.createWriteStream(`${name}-v${version}-${targetCpu}.zip`)) 27 | zip.end() 28 | -------------------------------------------------------------------------------- /scripts/download_libyue.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./common') 4 | 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | const downloadYue = require('download-yue') 9 | 10 | if (process.argv.length < 3) { 11 | console.error('Usage: download_libyue version platform') 12 | process.exit(1) 13 | } 14 | 15 | let version = process.argv[2] 16 | let platform = process.argv[3] 17 | 18 | if (!platform) { 19 | platform = { 20 | win32: 'win', 21 | linux: 'linux', 22 | darwin: 'mac', 23 | }[process.platform] 24 | } 25 | 26 | const libyueDir = path.resolve('libyue') 27 | const libyueChecksum = path.join(libyueDir, '.version') 28 | const checksum = `${version}|${platform}` 29 | if (fs.existsSync(libyueChecksum) && 30 | fs.readFileSync(libyueChecksum).toString() == checksum) { 31 | process.exit(0) 32 | } 33 | 34 | const filename = `libyue_${version}_${platform}.zip` 35 | downloadYue('yue', version, filename, libyueDir).then(() => { 36 | fs.writeFileSync(libyueChecksum, checksum) 37 | }) 38 | -------------------------------------------------------------------------------- /scripts/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {spawnSync} = require('./common') 4 | 5 | const name = require('../package.json').name 6 | const config = process.argv[2] ? process.argv[2] : 'Debug' 7 | 8 | if (process.platform == 'win32') { 9 | spawnSync(`out/${config}/${name}.exe`, []) 10 | } else if (process.platform == 'darwin') { 11 | spawnSync(`out/${config}/${name}`, []) 12 | } else { 13 | spawnSync(`out/${config}/${name}`, []) 14 | } 15 | -------------------------------------------------------------------------------- /src/exe.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yue/boilerplate-cpp/bc546946189575e7bbe7367c40ecd81908eb597f/src/exe.ico -------------------------------------------------------------------------------- /src/exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/exe.rc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Application icon 4 | 1 ICON exe.ico 5 | 6 | // Version resource 7 | VS_VERSION_INFO VERSIONINFO 8 | FILEFLAGSMASK 0x3fL 9 | #ifdef _DEBUG 10 | FILEFLAGS VS_FF_DEBUG 11 | #else 12 | FILEFLAGS 0x0L 13 | #endif 14 | 15 | FILEOS VOS_NT_WINDOWS32 16 | FILETYPE VFT_APP 17 | FILESUBTYPE 0x0L 18 | BEGIN 19 | BLOCK "StringFileInfo" 20 | BEGIN 21 | BLOCK "040904b0" 22 | BEGIN 23 | VALUE "CompanyName", "Yue" 24 | VALUE "ProductName", "Boilerplate" 25 | VALUE "FileDescription", "Build cross-platform desktop apps" 26 | VALUE "OriginalFilename", "boilerplate.exe" 27 | VALUE "InternalName", "boilerplate" 28 | VALUE "LegalCopyright", "Public domain" 29 | END 30 | END 31 | BLOCK "VarFileInfo" 32 | BEGIN 33 | VALUE "Translation", 0x409, 1200 34 | END 35 | END 36 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | // This file is published under public domain. 2 | 3 | #include "base/base_paths.h" 4 | #include "base/command_line.h" 5 | #include "base/cpu.h" 6 | #include "base/path_service.h" 7 | #include "nativeui/nativeui.h" 8 | 9 | // Generated from the ENCRYPTION_KEY file. 10 | #include "encryption_key.h" 11 | static_assert(sizeof(ENCRYPTION_KEY) == 16, "ENCRYPTION_KEY must be 16 bytes"); 12 | 13 | // Path to app's asar archive. 14 | static base::FilePath g_app_path; 15 | 16 | // Handle custom protocol. 17 | nu::ProtocolJob* CustomProtocolHandler(const std::string& url) { 18 | std::string path = url.substr(sizeof("boilerplate://app/") - 1); 19 | nu::ProtocolAsarJob* job = new nu::ProtocolAsarJob(g_app_path, path); 20 | job->SetDecipher(std::string(ENCRYPTION_KEY, sizeof(ENCRYPTION_KEY)), 21 | std::string("yue is good lib!")); 22 | return job; 23 | } 24 | 25 | // An example native binding. 26 | void ShowSysInfo(nu::Browser* browser, const std::string& request) { 27 | if (request == "cpu") { 28 | browser->ExecuteJavaScript( 29 | "window.report('" + base::CPU().cpu_brand() + "')", nullptr); 30 | } 31 | } 32 | 33 | #if defined(OS_WIN) 34 | int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { 35 | base::CommandLine::Init(0, nullptr); 36 | #else 37 | int main(int argc, const char *argv[]) { 38 | base::CommandLine::Init(argc, argv); 39 | #endif 40 | 41 | // In Debug build, load from app.ear; in Release build, load from exe path. 42 | #if defined(NDEBUG) 43 | base::PathService::Get(base::FILE_EXE, &g_app_path); 44 | #else 45 | base::PathService::Get(base::DIR_EXE, &g_app_path); 46 | #if defined(OS_WIN) 47 | g_app_path = g_app_path.DirName(); 48 | #endif 49 | g_app_path = g_app_path.Append(FILE_PATH_LITERAL("app.ear")); 50 | #endif 51 | 52 | // Intialize GUI toolkit. 53 | nu::Lifetime lifetime; 54 | 55 | // Initialize the global instance of nativeui. 56 | nu::State state; 57 | 58 | // Create window with default options. 59 | scoped_refptr window(new nu::Window(nu::Window::Options())); 60 | window->SetContentSize(nu::SizeF(400, 200)); 61 | window->Center(); 62 | 63 | // Quit when window is closed. 64 | window->on_close.Connect([](nu::Window*) { 65 | nu::MessageLoop::Quit(); 66 | }); 67 | 68 | // The content view. 69 | nu::Container* content_view = new nu::Container; 70 | window->SetContentView(content_view); 71 | 72 | #ifndef NDEBUG 73 | // Browser toolbar. 74 | nu::Container* toolbar = new nu::Container; 75 | toolbar->SetStyle("flex-direction", "row", "padding", 5.f); 76 | content_view->AddChildView(toolbar); 77 | nu::Button* go_back = new nu::Button("<"); 78 | go_back->SetEnabled(false); 79 | toolbar->AddChildView(go_back); 80 | nu::Button* go_forward = new nu::Button(">"); 81 | go_forward->SetEnabled(false); 82 | go_forward->SetStyle("margin-right", 5.f); 83 | toolbar->AddChildView(go_forward); 84 | nu::Entry* address_bar = new nu::Entry; 85 | address_bar->SetStyle("flex", 1.f); 86 | toolbar->AddChildView(address_bar); 87 | #endif 88 | 89 | // Create the webview. 90 | nu::Browser::Options options; 91 | #ifndef NDEBUG 92 | options.devtools = true; 93 | options.context_menu = true; 94 | #endif 95 | nu::Browser* browser(new nu::Browser(options)); 96 | browser->SetStyle("flex", 1.f); 97 | content_view->AddChildView(browser); 98 | 99 | #ifndef NDEBUG 100 | // Bind webview events to toolbar. 101 | browser->on_update_title.Connect([](nu::Browser* self, 102 | const std::string& title) { 103 | self->GetWindow()->SetTitle(title); 104 | }); 105 | browser->on_update_command.Connect([=](nu::Browser* self) { 106 | go_back->SetEnabled(self->CanGoBack()); 107 | go_forward->SetEnabled(self->CanGoForward()); 108 | address_bar->SetText(self->GetURL()); 109 | }); 110 | browser->on_commit_navigation.Connect([=](nu::Browser* self, 111 | const std::string& url) { 112 | address_bar->SetText(url); 113 | }); 114 | browser->on_finish_navigation.Connect([](nu::Browser* self, 115 | const std::string& url) { 116 | self->Focus(); 117 | }); 118 | 119 | // Bind toolbar events to browser. 120 | address_bar->on_activate.Connect([=](nu::Entry* self) { 121 | browser->LoadURL(self->GetText()); 122 | }); 123 | go_back->on_click.Connect([=](nu::Button* self) { 124 | browser->GoBack(); 125 | }); 126 | go_forward->on_click.Connect([=](nu::Button* self) { 127 | browser->GoForward(); 128 | }); 129 | #endif 130 | 131 | // Set webview bindings and custom protocol. 132 | nu::Browser::RegisterProtocol("boilerplate", &CustomProtocolHandler); 133 | browser->SetBindingName("boilerplate"); 134 | browser->AddBinding("showSysInfo", &ShowSysInfo); 135 | 136 | // Show window when page is loaded. 137 | int id = -1; 138 | id = browser->on_finish_navigation.Connect([=](nu::Browser* self, 139 | const std::string& url) { 140 | self->GetWindow()->Activate(); 141 | // Only activate for the first time. 142 | self->on_finish_navigation.Disconnect(id); 143 | }); 144 | browser->LoadURL("boilerplate://app/index.html"); 145 | 146 | // Enter message loop. 147 | nu::MessageLoop::Run(); 148 | return 0; 149 | } 150 | --------------------------------------------------------------------------------