mirror of https://github.com/tauri-apps/tauri
chore(merge) feature/bundle fork (#6)
* feat(proton) cargo-bundle fork & features argument * chore(bundle) rename lib to proton-bundle * feat(proton.h) merge open PRs from webview zserge/webview/pull/237 zserge/webview/pull/222 zserge/webview/pull/214 Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * feat(licenses) licenses and structure Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * addition to previous commit Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * fix(version) of proton-sys in cargo.toml Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * fix(various) use super: and remove 2018 Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * fix(various) Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * fix(rust-2018) update Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * fix(rust-2018) update Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * fix(rust-2018) add lockfiles Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * fix(proton) includedir errors on prod build * fix(rust-2018) authors, lockfiles and newest stuff Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * feat(cargo-proton-bundle) update readme Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev> * fix(proton) variable does not live long enough on dev * chore(proton) remove unnecessary global variable declaration
This commit is contained in:
parent
16f2464c26
commit
19c5d410de
|
@ -1,10 +1,7 @@
|
|||
test
|
||||
bindings
|
||||
docs
|
||||
lib
|
||||
node_modules
|
||||
spec
|
||||
ui
|
||||
.git
|
||||
.github
|
||||
.idea
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
.idea
|
|
@ -0,0 +1,127 @@
|
|||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "boxfnonce"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "0.4.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proton-sys"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proton-ui"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"boxfnonce 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proton-sys 1.0.0",
|
||||
"serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.15.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[metadata]
|
||||
"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd"
|
||||
"checksum boxfnonce 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426"
|
||||
"checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d"
|
||||
"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
|
||||
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
|
||||
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
|
||||
"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
|
||||
"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997"
|
||||
"checksum serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)" = "d46b3dfedb19360a74316866cef04687cd4d6a70df8e6a506c63512790769b72"
|
||||
"checksum serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)" = "c22a0820adfe2f257b098714323563dd06426502abbbce4f51b72ef544c5027f"
|
||||
"checksum serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "051c49229f282f7c6f3813f8286cc1e3323e8051823fce42c7ea80fe13521704"
|
||||
"checksum syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)" = "bc945221ccf4a7e8c31222b9d1fc77aefdd6638eb901a6ce457a3dc29d4c31e8"
|
||||
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||
"checksum urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3df3561629a8bb4c57e5a2e4c43348d9e29c7c29d9b1c4c1f47166deca8f37ed"
|
|
@ -1,17 +1,18 @@
|
|||
[package]
|
||||
name = "proton-ui"
|
||||
version = "0.1.0"
|
||||
authors = ["Boscop", "rstoenescu", "nothingismagick", "lucasfernog"]
|
||||
version = "1.0.0"
|
||||
authors = ["Lucas Fernandes Gonçalves Nogueira <lucas@quasar.dev>", "Daniel Thompson-Yvetot <denjell@quasar.dev>"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/quasarframework/proton"
|
||||
description = "Rust bindings for proton, a toolchain for building more secure native apps that have tiny binaries and are very fast."
|
||||
keywords = ["quasar", "web", "gui", "desktop", "webkit"]
|
||||
categories = ["quasar", "gui", "web-programming", "api-bindings", "rendering", "visualization"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
urlencoding = "1.0"
|
||||
proton-sys = { path = "proton-sys", version = "0.1.0" }
|
||||
proton-sys = { path = "proton-sys", version = "1.0.0" }
|
||||
boxfnonce = "0.1"
|
||||
|
||||
[features]
|
||||
|
@ -21,4 +22,4 @@ V1_30 = []
|
|||
[dev-dependencies]
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "proton-sys"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[metadata]
|
||||
"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd"
|
||||
"checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d"
|
||||
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "proton-sys"
|
||||
version = "0.1.0"
|
||||
authors = ["Boscop", "rstoenescu", "nothingismagick", "lucasfernog"]
|
||||
version = "1.0.0"
|
||||
authors = ["Boscop", "Lucas Fernandes Gonçalves Nogueira <lucas@quasar.dev>", "Daniel Thompson-Yvetot <denjell@quasar.dev>"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/quasarframework/proton"
|
||||
description = "Rust native ffi bindings for proton UI"
|
||||
|
@ -9,6 +9,7 @@ keywords = ["quasar", "web", "gui", "desktop", "webkit"]
|
|||
categories = ["quasar", "gui", "web-programming", "api-bindings", "rendering", "visualization"]
|
||||
build = "build.rs"
|
||||
links = "proton"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "proton_sys"
|
||||
|
@ -19,4 +20,4 @@ bitflags = "1.0"
|
|||
|
||||
[build-dependencies]
|
||||
cc = "1"
|
||||
pkg-config = "0.3"
|
||||
pkg-config = "0.3"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use ffi::{self, DialogFlags, DialogType};
|
||||
use crate::ffi::{self, DialogFlags, DialogType};
|
||||
use std::{ffi::CString, path::PathBuf};
|
||||
use {read_str, WVResult, WebView};
|
||||
use super::{read_str, WVResult, WebView};
|
||||
|
||||
const STR_BUF_SIZE: usize = 4096;
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@ mod color;
|
|||
mod dialog;
|
||||
mod error;
|
||||
mod escape;
|
||||
pub use color::Color;
|
||||
pub use dialog::DialogBuilder;
|
||||
pub use error::{CustomError, Error, WVResult};
|
||||
pub use escape::escape;
|
||||
pub use crate::color::Color;
|
||||
pub use crate::dialog::DialogBuilder;
|
||||
pub use crate::error::{CustomError, Error, WVResult};
|
||||
pub use crate::escape::escape;
|
||||
|
||||
use boxfnonce::SendBoxFnOnce;
|
||||
use ffi::*;
|
||||
use crate::ffi::*;
|
||||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
marker::PhantomData,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
[package]
|
||||
name = "proton"
|
||||
version = "0.1.0"
|
||||
authors = ["Lucas Fernandes Gonçalves Nogueira <lucas@quasar.dev>"]
|
||||
version = "1.0.0"
|
||||
authors = ["Lucas Fernandes Gonçalves Nogueira <lucas@quasar.dev>", "Daniel Thompson-Yvetot <denjell@quasar.dev>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
|
@ -24,4 +25,4 @@ either = "1.5.0"
|
|||
tar = "0.4"
|
||||
flate2 = "1"
|
||||
hyper-old-types = "0.11.0"
|
||||
sysinfo = "0.9"
|
||||
sysinfo = "0.9"
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Quasar app"
|
||||
authors = ["Lucas Fernandes Nogueira <lucasfernandesnog@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "A Quasar Proton App"
|
||||
author = []
|
||||
license = ""
|
||||
repository = ""
|
||||
build = "build.rs"
|
||||
include = ["data"]
|
||||
|
||||
|
|
|
@ -5,6 +5,11 @@ extern crate proton;
|
|||
extern crate proton_ui;
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
extern crate includedir;
|
||||
#[cfg(not(feature = "dev"))]
|
||||
extern crate phf;
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
extern crate tiny_http;
|
||||
|
||||
|
@ -22,7 +27,7 @@ mod server;
|
|||
fn main() {
|
||||
let debug;
|
||||
let content;
|
||||
let _matches: clap::ArgMatches;
|
||||
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
{
|
||||
|
@ -52,8 +57,8 @@ fn main() {
|
|||
.takes_value(true),
|
||||
);
|
||||
|
||||
_matches = app.get_matches();
|
||||
content = proton_ui::Content::Url(_matches.value_of("url").unwrap());
|
||||
let matches = app.get_matches();
|
||||
content = proton_ui::Content::Url(matches.value_of("url").unwrap().to_owned());
|
||||
debug = true;
|
||||
}
|
||||
#[cfg(not(feature = "dev"))]
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
target
|
||||
.DS_Store
|
||||
*.rs.bk
|
||||
*~
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,36 @@
|
|||
[package]
|
||||
name = "cargo-proton-bundle"
|
||||
version = "1.0.0"
|
||||
authors = ["George Burton <burtonageo@gmail.com>", "Lucas Fernandes Gonçalves Nogueira <lucas@quasar.dev>", "Daniel Thompson-Yvetot <denjell@quasar.dev>"]
|
||||
license = "MIT/Apache-2.0"
|
||||
keywords = ["bundle", "cargo", "proton", "quasar"]
|
||||
repository = "https://github.com/quasarframework/proton"
|
||||
description = "Wrap rust executables in OS-specific app bundles for Quasar-Proton"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
ar = "0.3"
|
||||
cab = "0.1"
|
||||
chrono = "0.4"
|
||||
clap = "^2"
|
||||
dirs = "1.0"
|
||||
error-chain = "0.12"
|
||||
glob = "0.2"
|
||||
icns = "^0.2"
|
||||
image = "0.12"
|
||||
libflate = "0.1"
|
||||
md5 = "0.3"
|
||||
msi = "0.2"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
strsim = "0.7"
|
||||
tar = "0.4"
|
||||
target_build_utils = "0.3"
|
||||
term = "0.4"
|
||||
toml = "0.4"
|
||||
uuid = { version = "0.5", features = ["v5"] }
|
||||
walkdir = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
winit = "0.11"
|
|
@ -0,0 +1,30 @@
|
|||
Copyright 2017 Cargo-Bundle developers
|
||||
|
||||
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.
|
||||
|
||||
-------
|
||||
|
||||
Copyright 2019 Quasar Framework
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
Copyright 2017 Cargo-Bundle developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
----
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Quasar Framework
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,136 @@
|
|||
# Cargo Proton Bundle
|
||||
|
||||
Wrap Rust executables in OS-specific app bundles
|
||||
|
||||
## About
|
||||
|
||||
`cargo proton-bundle` is a tool used to generate installers or app bundles for GUI
|
||||
executables built with `cargo`. It can create `.app` bundles for Mac OS X and
|
||||
iOS, `.deb` packages for Linux, and `.msi` installers for Windows (note however
|
||||
that iOS and Windows support is still experimental). Support for creating
|
||||
`.rpm` packages (for Linux) and `.apk` packages (for Android) is still pending.
|
||||
|
||||
To install `cargo proton-bundle`, run `cargo install cargo-proton-bundle`. This will add the most recent version of `cargo-bundle`
|
||||
published to [crates.io](https://crates.io/crates/cargo-bundle) as a subcommand to your default `cargo` installation.
|
||||
|
||||
To start using `cargo proton-bundle`, add a `[package.metadata.bundle]` section to your project's `Cargo.toml` file. This
|
||||
section describes various attributes of the generated bundle, such as its name, icon, description, copyright, as well
|
||||
as any packaging scripts you need to generate extra data. The full manifest format is described below.
|
||||
|
||||
To build a bundle for the OS you're on, simply run `cargo proton-bundle` in your
|
||||
project's directory (where the `Cargo.toml` is placed). If you would like to
|
||||
bundle a release build, you must add the `--release` flag to your call. To
|
||||
cross-compile and bundle an application for another OS, add an appropriate
|
||||
`--target` flag, just as you would for `cargo build`.
|
||||
|
||||
## Bundle manifest format
|
||||
|
||||
There are several fields in the `[package.metadata.bundle]` section.
|
||||
|
||||
### General settings
|
||||
|
||||
These settings apply to bundles for all (or most) OSes.
|
||||
|
||||
* `name`: The name of the built application. If this is not present, then it will use the `name` value from
|
||||
your `Cargo.toml` file.
|
||||
* `identifier`: [REQUIRED] A string that uniquely identifies your application,
|
||||
in reverse-DNS form (for example, `"com.example.appname"` or
|
||||
`"io.github.username.project"`). For OS X and iOS, this is used as the
|
||||
bundle's `CFBundleIdentifier` value; for Windows, this is hashed to create
|
||||
an application GUID.
|
||||
* `icon`: [OPTIONAL] The icons used for your application. This should be an array of file paths or globs (with images
|
||||
in various sizes/formats); `cargo-bundle` will automatically convert between image formats as necessary for
|
||||
different platforms. Supported formats include ICNS, ICO, PNG, and anything else that can be decoded by the
|
||||
[`image`](https://crates.io/crates/image) crate. Icons intended for high-resolution (e.g. Retina) displays
|
||||
should have a filename with `@2x` just before the extension (see example below).
|
||||
* `version`: [OPTIONAL] The version of the application. If this is not present, then it will use the `version`
|
||||
value from your `Cargo.toml` file.
|
||||
* `resources`: [OPTIONAL] List of files or directories which will be copied to the resources section of the
|
||||
bundle. Globs are supported.
|
||||
* `script`: [OPTIONAL] This is a reserved field; at the moment it is not used for anything, but may be used to
|
||||
run scripts while packaging the bundle (e.g. download files, compress and encrypt, etc.).
|
||||
* `copyright`: [OPTIONAL] This contains a copyright string associated with your application.
|
||||
* `category`: [OPTIONAL] What kind of application this is. This can
|
||||
be a human-readable string (e.g. `"Puzzle game"`), or a Mac OS X
|
||||
LSApplicationCategoryType value
|
||||
(e.g. `"public.app-category.puzzle-games"`), or a GNOME desktop
|
||||
file category name (e.g. `"LogicGame"`), and `cargo-bundle` will
|
||||
automatically convert as needed for different platforms.
|
||||
* `short_description`: [OPTIONAL] A short, one-line description of the application. If this is not present, then it
|
||||
will use the `description` value from your `Cargo.toml` file.
|
||||
* `long_description`: [OPTIONAL] A longer, multi-line description of the application.
|
||||
|
||||
### Debian-specific settings
|
||||
|
||||
These settings are used only when bundling `deb` packages.
|
||||
|
||||
* `deb_depends`: A list of strings indicating other packages (e.g. shared
|
||||
libraries) that this package depends on to be installed. If present, this
|
||||
forms the `Depends:` field of the `deb` package control file.
|
||||
|
||||
### Mac OS X-specific settings
|
||||
|
||||
These settings are used only when bundling `osx` packages.
|
||||
|
||||
* `osx_frameworks`: A list of strings indicating any Mac OS X frameworks that
|
||||
need to be bundled with the app. Each string can either be the name of a
|
||||
framework (without the `.framework` extension, e.g. `"SDL2"`), in which case
|
||||
`cargo-bundle` will search for that framework in the standard install
|
||||
locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and
|
||||
`/Network/Library/Frameworks/`), or a path to a specific framework bundle
|
||||
(e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes
|
||||
`cargo-bundle` copy the specified frameworks into the OS X app bundle (under
|
||||
`Foobar.app/Contents/Frameworks/`); you are still responsible for (1)
|
||||
arranging for the compiled binary to link against those frameworks (e.g. by
|
||||
emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your
|
||||
`build.rs` script), and (2) embedding the correct rpath in your binary
|
||||
(e.g. by running `install_name_tool -add_rpath
|
||||
"@executable_path/../Frameworks" path/to/binary` after compiling).
|
||||
* `osx_minimum_system_version`: A version string indicating the minimum Mac OS
|
||||
X version that the bundled app supports (e.g. `"10.11"`). If you are using
|
||||
this config field, you may also want have your `build.rs` script emit
|
||||
`cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11` (or whatever version number
|
||||
you want) to ensure that the compiled binary has the same minimum version.
|
||||
|
||||
### Example `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "example"
|
||||
# ...other fields...
|
||||
|
||||
[package.metadata.bundle]
|
||||
name = "ExampleApplication"
|
||||
identifier = "com.doe.exampleapplication"
|
||||
icon = ["32x32.png", "128x128.png", "128x128@2x.png"]
|
||||
version = "1.0.0"
|
||||
resources = ["assets", "images/**/*.png", "secrets/public_key.txt"]
|
||||
copyright = "Copyright (c) Jane Doe 2016. All rights reserved."
|
||||
category = "Developer Tool"
|
||||
short_description = "An example application."
|
||||
long_description = """
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat.
|
||||
"""
|
||||
deb_depends = ["libgl1-mesa-glx", "libsdl2-2.0-0 (>= 2.0.5)"]
|
||||
osx_frameworks = ["SDL2"]
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
`cargo-proton-bundle` has ambitions to be inclusive project and welcome contributions from anyone. Please abide by the Rust
|
||||
code of conduct.
|
||||
|
||||
## Status
|
||||
|
||||
Very early alpha. Expect the format of the `[package.metadata.bundle]` section to change, and there is no guarantee of
|
||||
stability.
|
||||
|
||||
## License
|
||||
(c) 2017 - present, George Burton, Lucas Fernandes Gonçalves Nogueira, Daniel Thompson-Yvetot, Razvan Stoenescu
|
||||
|
||||
This program is licensed either under the terms of the
|
||||
[Apache Software License](http://www.apache.org/licenses/LICENSE-2.0), or the
|
||||
[MIT License](https://opensource.org/licenses/MIT).
|
|
@ -0,0 +1,13 @@
|
|||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2015"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
|
@ -0,0 +1,455 @@
|
|||
use serde;
|
||||
use std::fmt;
|
||||
use strsim;
|
||||
|
||||
const CONFIDENCE_THRESHOLD: f64 = 0.8;
|
||||
|
||||
const OSX_APP_CATEGORY_PREFIX: &str = "public.app-category.";
|
||||
|
||||
// TODO: RIght now, these categories correspond to LSApplicationCategoryType
|
||||
// values for OS X. There are also some additional GNOME registered categories
|
||||
// that don't fit these; we should add those here too.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum AppCategory {
|
||||
Business,
|
||||
DeveloperTool,
|
||||
Education,
|
||||
Entertainment,
|
||||
Finance,
|
||||
Game,
|
||||
ActionGame,
|
||||
AdventureGame,
|
||||
ArcadeGame,
|
||||
BoardGame,
|
||||
CardGame,
|
||||
CasinoGame,
|
||||
DiceGame,
|
||||
EducationalGame,
|
||||
FamilyGame,
|
||||
KidsGame,
|
||||
MusicGame,
|
||||
PuzzleGame,
|
||||
RacingGame,
|
||||
RolePlayingGame,
|
||||
SimulationGame,
|
||||
SportsGame,
|
||||
StrategyGame,
|
||||
TriviaGame,
|
||||
WordGame,
|
||||
GraphicsAndDesign,
|
||||
HealthcareAndFitness,
|
||||
Lifestyle,
|
||||
Medical,
|
||||
Music,
|
||||
News,
|
||||
Photography,
|
||||
Productivity,
|
||||
Reference,
|
||||
SocialNetworking,
|
||||
Sports,
|
||||
Travel,
|
||||
Utility,
|
||||
Video,
|
||||
Weather,
|
||||
}
|
||||
|
||||
impl AppCategory {
|
||||
/// Given a string, returns the `AppCategory` it refers to, or the closest
|
||||
/// string that the user might have intended (if any).
|
||||
pub fn from_str(input: &str) -> Result<AppCategory, Option<&'static str>> {
|
||||
// Canonicalize input:
|
||||
let mut input = input.to_ascii_lowercase();
|
||||
if input.starts_with(OSX_APP_CATEGORY_PREFIX) {
|
||||
input = input.split_at(OSX_APP_CATEGORY_PREFIX.len()).1.to_string();
|
||||
}
|
||||
input = input.replace(" ", "");
|
||||
input = input.replace("-", "");
|
||||
|
||||
// Find best match:
|
||||
let mut best_confidence = 0.0;
|
||||
let mut best_category: Option<AppCategory> = None;
|
||||
for &(string, category) in CATEGORY_STRINGS.iter() {
|
||||
if input == string {
|
||||
return Ok(category);
|
||||
}
|
||||
let confidence = strsim::jaro_winkler(&input, string);
|
||||
if confidence >= CONFIDENCE_THRESHOLD && confidence > best_confidence {
|
||||
best_confidence = confidence;
|
||||
best_category = Some(category);
|
||||
}
|
||||
}
|
||||
Err(best_category.map(AppCategory::canonical))
|
||||
}
|
||||
|
||||
/// Map an AppCategory to the string we recommend to use in Cargo.toml if
|
||||
/// the users misspells the category name.
|
||||
fn canonical(self) -> &'static str {
|
||||
match self {
|
||||
AppCategory::Business => "Business",
|
||||
AppCategory::DeveloperTool => "Developer Tool",
|
||||
AppCategory::Education => "Education",
|
||||
AppCategory::Entertainment => "Entertainment",
|
||||
AppCategory::Finance => "Finance",
|
||||
AppCategory::Game => "Game",
|
||||
AppCategory::ActionGame => "Action Game",
|
||||
AppCategory::AdventureGame => "Adventure Game",
|
||||
AppCategory::ArcadeGame => "Arcade Game",
|
||||
AppCategory::BoardGame => "Board Game",
|
||||
AppCategory::CardGame => "Card Game",
|
||||
AppCategory::CasinoGame => "Casino Game",
|
||||
AppCategory::DiceGame => "Dice Game",
|
||||
AppCategory::EducationalGame => "Educational Game",
|
||||
AppCategory::FamilyGame => "Family Game",
|
||||
AppCategory::KidsGame => "Kids Game",
|
||||
AppCategory::MusicGame => "Music Game",
|
||||
AppCategory::PuzzleGame => "Puzzle Game",
|
||||
AppCategory::RacingGame => "Racing Game",
|
||||
AppCategory::RolePlayingGame => "Role-Playing Game",
|
||||
AppCategory::SimulationGame => "Simulation Game",
|
||||
AppCategory::SportsGame => "Sports Game",
|
||||
AppCategory::StrategyGame => "Strategy Game",
|
||||
AppCategory::TriviaGame => "Trivia Game",
|
||||
AppCategory::WordGame => "Word Game",
|
||||
AppCategory::GraphicsAndDesign => "Graphics and Design",
|
||||
AppCategory::HealthcareAndFitness => "Healthcare and Fitness",
|
||||
AppCategory::Lifestyle => "Lifestyle",
|
||||
AppCategory::Medical => "Medical",
|
||||
AppCategory::Music => "Music",
|
||||
AppCategory::News => "News",
|
||||
AppCategory::Photography => "Photography",
|
||||
AppCategory::Productivity => "Productivity",
|
||||
AppCategory::Reference => "Reference",
|
||||
AppCategory::SocialNetworking => "Social Networking",
|
||||
AppCategory::Sports => "Sports",
|
||||
AppCategory::Travel => "Travel",
|
||||
AppCategory::Utility => "Utility",
|
||||
AppCategory::Video => "Video",
|
||||
AppCategory::Weather => "Weather",
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an AppCategory to the closest set of GNOME desktop registered
|
||||
/// categories that matches that category.
|
||||
pub fn gnome_desktop_categories(&self) -> &'static str {
|
||||
match &self {
|
||||
AppCategory::Business => "Office;",
|
||||
AppCategory::DeveloperTool => "Development;",
|
||||
AppCategory::Education => "Education;",
|
||||
AppCategory::Entertainment => "Network;",
|
||||
AppCategory::Finance => "Office;Finance;",
|
||||
AppCategory::Game => "Game;",
|
||||
AppCategory::ActionGame => "Game;ActionGame;",
|
||||
AppCategory::AdventureGame => "Game;AdventureGame;",
|
||||
AppCategory::ArcadeGame => "Game;ArcadeGame;",
|
||||
AppCategory::BoardGame => "Game;BoardGame;",
|
||||
AppCategory::CardGame => "Game;CardGame;",
|
||||
AppCategory::CasinoGame => "Game;",
|
||||
AppCategory::DiceGame => "Game;",
|
||||
AppCategory::EducationalGame => "Game;Education;",
|
||||
AppCategory::FamilyGame => "Game;",
|
||||
AppCategory::KidsGame => "Game;KidsGame;",
|
||||
AppCategory::MusicGame => "Game;",
|
||||
AppCategory::PuzzleGame => "Game;LogicGame;",
|
||||
AppCategory::RacingGame => "Game;",
|
||||
AppCategory::RolePlayingGame => "Game;RolePlaying;",
|
||||
AppCategory::SimulationGame => "Game;Simulation;",
|
||||
AppCategory::SportsGame => "Game;SportsGame;",
|
||||
AppCategory::StrategyGame => "Game;StrategyGame;",
|
||||
AppCategory::TriviaGame => "Game;",
|
||||
AppCategory::WordGame => "Game;",
|
||||
AppCategory::GraphicsAndDesign => "Graphics;",
|
||||
AppCategory::HealthcareAndFitness => "Science;",
|
||||
AppCategory::Lifestyle => "Education;",
|
||||
AppCategory::Medical => "Science;MedicalSoftware;",
|
||||
AppCategory::Music => "AudioVideo;Audio;Music;",
|
||||
AppCategory::News => "Network;News;",
|
||||
AppCategory::Photography => "Graphics;Photography;",
|
||||
AppCategory::Productivity => "Office;",
|
||||
AppCategory::Reference => "Education;",
|
||||
AppCategory::SocialNetworking => "Network;",
|
||||
AppCategory::Sports => "Education;Sports;",
|
||||
AppCategory::Travel => "Education;",
|
||||
AppCategory::Utility => "Utility;",
|
||||
AppCategory::Video => "AudioVideo;Video;",
|
||||
AppCategory::Weather => "Science;",
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an AppCategory to the closest LSApplicationCategoryType value that
|
||||
/// matches that category.
|
||||
pub fn osx_application_category_type(&self) -> &'static str {
|
||||
match &self {
|
||||
AppCategory::Business => "public.app-category.business",
|
||||
AppCategory::DeveloperTool => "public.app-category.developer-tools",
|
||||
AppCategory::Education => "public.app-category.education",
|
||||
AppCategory::Entertainment => "public.app-category.entertainment",
|
||||
AppCategory::Finance => "public.app-category.finance",
|
||||
AppCategory::Game => "public.app-category.games",
|
||||
AppCategory::ActionGame => "public.app-category.action-games",
|
||||
AppCategory::AdventureGame => "public.app-category.adventure-games",
|
||||
AppCategory::ArcadeGame => "public.app-category.arcade-games",
|
||||
AppCategory::BoardGame => "public.app-category.board-games",
|
||||
AppCategory::CardGame => "public.app-category.card-games",
|
||||
AppCategory::CasinoGame => "public.app-category.casino-games",
|
||||
AppCategory::DiceGame => "public.app-category.dice-games",
|
||||
AppCategory::EducationalGame => "public.app-category.educational-games",
|
||||
AppCategory::FamilyGame => "public.app-category.family-games",
|
||||
AppCategory::KidsGame => "public.app-category.kids-games",
|
||||
AppCategory::MusicGame => "public.app-category.music-games",
|
||||
AppCategory::PuzzleGame => "public.app-category.puzzle-games",
|
||||
AppCategory::RacingGame => "public.app-category.racing-games",
|
||||
AppCategory::RolePlayingGame => "public.app-category.role-playing-games",
|
||||
AppCategory::SimulationGame => "public.app-category.simulation-games",
|
||||
AppCategory::SportsGame => "public.app-category.sports-games",
|
||||
AppCategory::StrategyGame => "public.app-category.strategy-games",
|
||||
AppCategory::TriviaGame => "public.app-category.trivia-games",
|
||||
AppCategory::WordGame => "public.app-category.word-games",
|
||||
AppCategory::GraphicsAndDesign => "public.app-category.graphics-design",
|
||||
AppCategory::HealthcareAndFitness => "public.app-category.healthcare-fitness",
|
||||
AppCategory::Lifestyle => "public.app-category.lifestyle",
|
||||
AppCategory::Medical => "public.app-category.medical",
|
||||
AppCategory::Music => "public.app-category.music",
|
||||
AppCategory::News => "public.app-category.news",
|
||||
AppCategory::Photography => "public.app-category.photography",
|
||||
AppCategory::Productivity => "public.app-category.productivity",
|
||||
AppCategory::Reference => "public.app-category.reference",
|
||||
AppCategory::SocialNetworking => "public.app-category.social-networking",
|
||||
AppCategory::Sports => "public.app-category.sports",
|
||||
AppCategory::Travel => "public.app-category.travel",
|
||||
AppCategory::Utility => "public.app-category.utilities",
|
||||
AppCategory::Video => "public.app-category.video",
|
||||
AppCategory::Weather => "public.app-category.weather",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> serde::Deserialize<'d> for AppCategory {
|
||||
fn deserialize<D: serde::Deserializer<'d>>(deserializer: D) -> Result<AppCategory, D::Error> {
|
||||
deserializer.deserialize_str(AppCategoryVisitor { did_you_mean: None })
|
||||
}
|
||||
}
|
||||
|
||||
struct AppCategoryVisitor {
|
||||
did_you_mean: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl<'d> serde::de::Visitor<'d> for AppCategoryVisitor {
|
||||
type Value = AppCategory;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self.did_you_mean {
|
||||
Some(string) => write!(
|
||||
formatter,
|
||||
"a valid app category string (did you mean \"{}\"?)",
|
||||
string
|
||||
),
|
||||
None => write!(formatter, "a valid app category string"),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(mut self, value: &str) -> Result<AppCategory, E> {
|
||||
match AppCategory::from_str(value) {
|
||||
Ok(category) => Ok(category),
|
||||
Err(did_you_mean) => {
|
||||
self.did_you_mean = did_you_mean;
|
||||
let unexp = serde::de::Unexpected::Str(value);
|
||||
Err(serde::de::Error::invalid_value(unexp, &self))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CATEGORY_STRINGS: &[(&str, AppCategory)] = &[
|
||||
("actiongame", AppCategory::ActionGame),
|
||||
("actiongames", AppCategory::ActionGame),
|
||||
("adventuregame", AppCategory::AdventureGame),
|
||||
("adventuregames", AppCategory::AdventureGame),
|
||||
("arcadegame", AppCategory::ArcadeGame),
|
||||
("arcadegames", AppCategory::ArcadeGame),
|
||||
("boardgame", AppCategory::BoardGame),
|
||||
("boardgames", AppCategory::BoardGame),
|
||||
("business", AppCategory::Business),
|
||||
("cardgame", AppCategory::CardGame),
|
||||
("cardgames", AppCategory::CardGame),
|
||||
("casinogame", AppCategory::CasinoGame),
|
||||
("casinogames", AppCategory::CasinoGame),
|
||||
("developer", AppCategory::DeveloperTool),
|
||||
("developertool", AppCategory::DeveloperTool),
|
||||
("developertools", AppCategory::DeveloperTool),
|
||||
("development", AppCategory::DeveloperTool),
|
||||
("dicegame", AppCategory::DiceGame),
|
||||
("dicegames", AppCategory::DiceGame),
|
||||
("education", AppCategory::Education),
|
||||
("educationalgame", AppCategory::EducationalGame),
|
||||
("educationalgames", AppCategory::EducationalGame),
|
||||
("entertainment", AppCategory::Entertainment),
|
||||
("familygame", AppCategory::FamilyGame),
|
||||
("familygames", AppCategory::FamilyGame),
|
||||
("finance", AppCategory::Finance),
|
||||
("fitness", AppCategory::HealthcareAndFitness),
|
||||
("game", AppCategory::Game),
|
||||
("games", AppCategory::Game),
|
||||
("graphicdesign", AppCategory::GraphicsAndDesign),
|
||||
("graphicsanddesign", AppCategory::GraphicsAndDesign),
|
||||
("graphicsdesign", AppCategory::GraphicsAndDesign),
|
||||
("healthcareandfitness", AppCategory::HealthcareAndFitness),
|
||||
("healthcarefitness", AppCategory::HealthcareAndFitness),
|
||||
("kidsgame", AppCategory::KidsGame),
|
||||
("kidsgames", AppCategory::KidsGame),
|
||||
("lifestyle", AppCategory::Lifestyle),
|
||||
("logicgame", AppCategory::PuzzleGame),
|
||||
("medical", AppCategory::Medical),
|
||||
("medicalsoftware", AppCategory::Medical),
|
||||
("music", AppCategory::Music),
|
||||
("musicgame", AppCategory::MusicGame),
|
||||
("musicgames", AppCategory::MusicGame),
|
||||
("news", AppCategory::News),
|
||||
("photography", AppCategory::Photography),
|
||||
("productivity", AppCategory::Productivity),
|
||||
("puzzlegame", AppCategory::PuzzleGame),
|
||||
("puzzlegames", AppCategory::PuzzleGame),
|
||||
("racinggame", AppCategory::RacingGame),
|
||||
("racinggames", AppCategory::RacingGame),
|
||||
("reference", AppCategory::Reference),
|
||||
("roleplaying", AppCategory::RolePlayingGame),
|
||||
("roleplayinggame", AppCategory::RolePlayingGame),
|
||||
("roleplayinggames", AppCategory::RolePlayingGame),
|
||||
("rpg", AppCategory::RolePlayingGame),
|
||||
("simulationgame", AppCategory::SimulationGame),
|
||||
("simulationgames", AppCategory::SimulationGame),
|
||||
("socialnetwork", AppCategory::SocialNetworking),
|
||||
("socialnetworking", AppCategory::SocialNetworking),
|
||||
("sports", AppCategory::Sports),
|
||||
("sportsgame", AppCategory::SportsGame),
|
||||
("sportsgames", AppCategory::SportsGame),
|
||||
("strategygame", AppCategory::StrategyGame),
|
||||
("strategygames", AppCategory::StrategyGame),
|
||||
("travel", AppCategory::Travel),
|
||||
("triviagame", AppCategory::TriviaGame),
|
||||
("triviagames", AppCategory::TriviaGame),
|
||||
("utilities", AppCategory::Utility),
|
||||
("utility", AppCategory::Utility),
|
||||
("video", AppCategory::Video),
|
||||
("weather", AppCategory::Weather),
|
||||
("wordgame", AppCategory::WordGame),
|
||||
("wordgames", AppCategory::WordGame),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AppCategory;
|
||||
|
||||
#[test]
|
||||
fn category_from_string_ok() {
|
||||
// Canonical name of category works:
|
||||
assert_eq!(
|
||||
AppCategory::from_str("Education"),
|
||||
Ok(AppCategory::Education)
|
||||
);
|
||||
assert_eq!(
|
||||
AppCategory::from_str("Developer Tool"),
|
||||
Ok(AppCategory::DeveloperTool)
|
||||
);
|
||||
// Lowercase, spaces, and hypens are fine:
|
||||
assert_eq!(
|
||||
AppCategory::from_str(" puzzle game "),
|
||||
Ok(AppCategory::PuzzleGame)
|
||||
);
|
||||
assert_eq!(
|
||||
AppCategory::from_str("Role-playing game"),
|
||||
Ok(AppCategory::RolePlayingGame)
|
||||
);
|
||||
// Using macOS LSApplicationCategoryType value is fine:
|
||||
assert_eq!(
|
||||
AppCategory::from_str("public.app-category.developer-tools"),
|
||||
Ok(AppCategory::DeveloperTool)
|
||||
);
|
||||
assert_eq!(
|
||||
AppCategory::from_str("public.app-category.role-playing-games"),
|
||||
Ok(AppCategory::RolePlayingGame)
|
||||
);
|
||||
// Using GNOME category name is fine:
|
||||
assert_eq!(
|
||||
AppCategory::from_str("Development"),
|
||||
Ok(AppCategory::DeveloperTool)
|
||||
);
|
||||
assert_eq!(
|
||||
AppCategory::from_str("LogicGame"),
|
||||
Ok(AppCategory::PuzzleGame)
|
||||
);
|
||||
// Using common abbreviations is fine:
|
||||
assert_eq!(
|
||||
AppCategory::from_str("RPG"),
|
||||
Ok(AppCategory::RolePlayingGame)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_from_string_did_you_mean() {
|
||||
assert_eq!(AppCategory::from_str("gaming"), Err(Some("Game")));
|
||||
assert_eq!(AppCategory::from_str("photos"), Err(Some("Photography")));
|
||||
assert_eq!(
|
||||
AppCategory::from_str("strategery"),
|
||||
Err(Some("Strategy Game"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_from_string_totally_wrong() {
|
||||
assert_eq!(AppCategory::from_str("fhqwhgads"), Err(None));
|
||||
assert_eq!(AppCategory::from_str("WHARRGARBL"), Err(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ls_application_category_type_round_trip() {
|
||||
let values = &[
|
||||
"public.app-category.business",
|
||||
"public.app-category.developer-tools",
|
||||
"public.app-category.education",
|
||||
"public.app-category.entertainment",
|
||||
"public.app-category.finance",
|
||||
"public.app-category.games",
|
||||
"public.app-category.action-games",
|
||||
"public.app-category.adventure-games",
|
||||
"public.app-category.arcade-games",
|
||||
"public.app-category.board-games",
|
||||
"public.app-category.card-games",
|
||||
"public.app-category.casino-games",
|
||||
"public.app-category.dice-games",
|
||||
"public.app-category.educational-games",
|
||||
"public.app-category.family-games",
|
||||
"public.app-category.kids-games",
|
||||
"public.app-category.music-games",
|
||||
"public.app-category.puzzle-games",
|
||||
"public.app-category.racing-games",
|
||||
"public.app-category.role-playing-games",
|
||||
"public.app-category.simulation-games",
|
||||
"public.app-category.sports-games",
|
||||
"public.app-category.strategy-games",
|
||||
"public.app-category.trivia-games",
|
||||
"public.app-category.word-games",
|
||||
"public.app-category.graphics-design",
|
||||
"public.app-category.healthcare-fitness",
|
||||
"public.app-category.lifestyle",
|
||||
"public.app-category.medical",
|
||||
"public.app-category.music",
|
||||
"public.app-category.news",
|
||||
"public.app-category.photography",
|
||||
"public.app-category.productivity",
|
||||
"public.app-category.reference",
|
||||
"public.app-category.social-networking",
|
||||
"public.app-category.sports",
|
||||
"public.app-category.travel",
|
||||
"public.app-category.utilities",
|
||||
"public.app-category.video",
|
||||
"public.app-category.weather",
|
||||
];
|
||||
// Test that if the user uses an LSApplicationCategoryType string as
|
||||
// the category string, they will get back that same string for the
|
||||
// macOS app bundle LSApplicationCategoryType.
|
||||
for &value in values.iter() {
|
||||
let category = AppCategory::from_str(value).expect(value);
|
||||
assert_eq!(category.osx_application_category_type(), value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,314 @@
|
|||
use std;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, BufWriter, Write};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use term;
|
||||
use walkdir;
|
||||
use crate::ResultExt;
|
||||
|
||||
/// Returns true if the path has a filename indicating that it is a high-desity
|
||||
/// "retina" icon. Specifically, returns true the the file stem ends with
|
||||
/// "@2x" (a convention specified by the [Apple developer docs](
|
||||
/// https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html)).
|
||||
pub fn is_retina<P: AsRef<Path>>(path: P) -> bool {
|
||||
path
|
||||
.as_ref()
|
||||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|stem| stem.ends_with("@2x"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Creates a new file at the given path, creating any parent directories as
|
||||
/// needed.
|
||||
pub fn create_file(path: &Path) -> crate::Result<BufWriter<File>> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(&parent).chain_err(|| format!("Failed to create directory {:?}", parent))?;
|
||||
}
|
||||
let file = File::create(path).chain_err(|| format!("Failed to create file {:?}", path))?;
|
||||
Ok(BufWriter::new(file))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
std::os::unix::fs::symlink(src, dst)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
std::os::windows::fs::symlink_dir(src, dst)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
std::os::unix::fs::symlink(src, dst)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
std::os::windows::fs::symlink_file(src, dst)
|
||||
}
|
||||
|
||||
/// Copies a regular file from one path to another, creating any parent
|
||||
/// directories of the destination path as necessary. Fails if the source path
|
||||
/// is a directory or doesn't exist.
|
||||
pub fn copy_file(from: &Path, to: &Path) -> crate::Result<()> {
|
||||
if !from.exists() {
|
||||
bail!("{:?} does not exist", from);
|
||||
}
|
||||
if !from.is_file() {
|
||||
bail!("{:?} is not a file", from);
|
||||
}
|
||||
let dest_dir = to.parent().unwrap();
|
||||
fs::create_dir_all(dest_dir).chain_err(|| format!("Failed to create {:?}", dest_dir))?;
|
||||
fs::copy(from, to).chain_err(|| format!("Failed to copy {:?} to {:?}", from, to))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recursively copies a directory file from one path to another, creating any
|
||||
/// parent directories of the destination path as necessary. Fails if the
|
||||
/// source path is not a directory or doesn't exist, or if the destination path
|
||||
/// already exists.
|
||||
pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
|
||||
if !from.exists() {
|
||||
bail!("{:?} does not exist", from);
|
||||
}
|
||||
if !from.is_dir() {
|
||||
bail!("{:?} is not a directory", from);
|
||||
}
|
||||
if to.exists() {
|
||||
bail!("{:?} already exists", to);
|
||||
}
|
||||
let parent = to.parent().unwrap();
|
||||
fs::create_dir_all(parent).chain_err(|| format!("Failed to create {:?}", parent))?;
|
||||
for entry in walkdir::WalkDir::new(from) {
|
||||
let entry = entry?;
|
||||
debug_assert!(entry.path().starts_with(from));
|
||||
let rel_path = entry.path().strip_prefix(from).unwrap();
|
||||
let dest_path = to.join(rel_path);
|
||||
if entry.file_type().is_symlink() {
|
||||
let target = fs::read_link(entry.path())?;
|
||||
if entry.path().is_dir() {
|
||||
symlink_dir(&target, &dest_path)?;
|
||||
} else {
|
||||
symlink_file(&target, &dest_path)?;
|
||||
}
|
||||
} else if entry.file_type().is_dir() {
|
||||
fs::create_dir(dest_path)?;
|
||||
} else {
|
||||
fs::copy(entry.path(), dest_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Given a path (absolute or relative) to a resource file, returns the
|
||||
/// relative path from the bundle resources directory where that resource
|
||||
/// should be stored.
|
||||
pub fn resource_relpath(path: &Path) -> PathBuf {
|
||||
let mut dest = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(_) => {}
|
||||
Component::RootDir => dest.push("_root_"),
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => dest.push("_up_"),
|
||||
Component::Normal(string) => dest.push(string),
|
||||
}
|
||||
}
|
||||
dest
|
||||
}
|
||||
|
||||
/// Prints a message to stderr, in the same format that `cargo` uses,
|
||||
/// indicating that we are creating a bundle with the given filename.
|
||||
pub fn print_bundling(filename: &str) -> crate::Result<()> {
|
||||
print_progress("Bundling", filename)
|
||||
}
|
||||
|
||||
/// Prints a message to stderr, in the same format that `cargo` uses,
|
||||
/// indicating that we have finished the the given bundles.
|
||||
pub fn print_finished(output_paths: &Vec<PathBuf>) -> crate::Result<()> {
|
||||
let pluralised = if output_paths.len() == 1 {
|
||||
"bundle"
|
||||
} else {
|
||||
"bundles"
|
||||
};
|
||||
let msg = format!("{} {} at:", output_paths.len(), pluralised);
|
||||
print_progress("Finished", &msg)?;
|
||||
for path in output_paths {
|
||||
println!(" {}", path.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn safe_term_attr<T: term::Terminal + ?Sized>(
|
||||
output: &mut Box<T>,
|
||||
attr: term::Attr,
|
||||
) -> term::Result<()> {
|
||||
match output.supports_attr(attr) {
|
||||
true => output.attr(attr),
|
||||
false => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_progress(step: &str, msg: &str) -> crate::Result<()> {
|
||||
if let Some(mut output) = term::stderr() {
|
||||
safe_term_attr(&mut output, term::Attr::Bold)?;
|
||||
output.fg(term::color::GREEN)?;
|
||||
write!(output, " {}", step)?;
|
||||
output.reset()?;
|
||||
write!(output, " {}\n", msg)?;
|
||||
output.flush()?;
|
||||
Ok(())
|
||||
} else {
|
||||
let mut output = io::stderr();
|
||||
write!(output, " {}", step)?;
|
||||
write!(output, " {}\n", msg)?;
|
||||
output.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints a warning message to stderr, in the same format that `cargo` uses.
|
||||
pub fn print_warning(message: &str) -> crate::Result<()> {
|
||||
if let Some(mut output) = term::stderr() {
|
||||
safe_term_attr(&mut output, term::Attr::Bold)?;
|
||||
output.fg(term::color::YELLOW)?;
|
||||
write!(output, "warning:")?;
|
||||
output.reset()?;
|
||||
write!(output, " {}\n", message)?;
|
||||
output.flush()?;
|
||||
Ok(())
|
||||
} else {
|
||||
let mut output = io::stderr();
|
||||
write!(output, "warning:")?;
|
||||
write!(output, " {}\n", message)?;
|
||||
output.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints an error to stderr, in the same format that `cargo` uses.
|
||||
pub fn print_error(error: &crate::Error) -> crate::Result<()> {
|
||||
if let Some(mut output) = term::stderr() {
|
||||
safe_term_attr(&mut output, term::Attr::Bold)?;
|
||||
output.fg(term::color::RED)?;
|
||||
write!(output, "error:")?;
|
||||
output.reset()?;
|
||||
safe_term_attr(&mut output, term::Attr::Bold)?;
|
||||
writeln!(output, " {}", error)?;
|
||||
output.reset()?;
|
||||
for cause in error.iter().skip(1) {
|
||||
writeln!(output, " Caused by: {}", cause)?;
|
||||
}
|
||||
if let Some(backtrace) = error.backtrace() {
|
||||
writeln!(output, "{:?}", backtrace)?;
|
||||
}
|
||||
output.flush()?;
|
||||
Ok(())
|
||||
} else {
|
||||
let mut output = io::stderr();
|
||||
write!(output, "error:")?;
|
||||
writeln!(output, " {}", error)?;
|
||||
for cause in error.iter().skip(1) {
|
||||
writeln!(output, " Caused by: {}", cause)?;
|
||||
}
|
||||
if let Some(backtrace) = error.backtrace() {
|
||||
writeln!(output, "{:?}", backtrace)?;
|
||||
}
|
||||
output.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{copy_dir, create_file, is_retina, resource_relpath, symlink_file};
|
||||
use std;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempfile;
|
||||
|
||||
#[test]
|
||||
fn create_file_with_parent_dirs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
assert!(!tmp.path().join("parent").exists());
|
||||
{
|
||||
let mut file = create_file(&tmp.path().join("parent/file.txt")).unwrap();
|
||||
write!(file, "Hello, world!\n").unwrap();
|
||||
}
|
||||
assert!(tmp.path().join("parent").is_dir());
|
||||
assert!(tmp.path().join("parent/file.txt").is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_dir_with_symlinks() {
|
||||
// Create a directory structure that looks like this:
|
||||
// ${TMP}/orig/
|
||||
// sub/
|
||||
// file.txt
|
||||
// link -> sub/file.txt
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
{
|
||||
let mut file = create_file(&tmp.path().join("orig/sub/file.txt")).unwrap();
|
||||
write!(file, "Hello, world!\n").unwrap();
|
||||
}
|
||||
symlink_file(
|
||||
&PathBuf::from("sub/file.txt"),
|
||||
&tmp.path().join("orig/link"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read(tmp.path().join("orig/link"))
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
b"Hello, world!\n"
|
||||
);
|
||||
// Copy ${TMP}/orig to ${TMP}/parent/copy, and make sure that the
|
||||
// directory structure, file, and symlink got copied correctly.
|
||||
copy_dir(&tmp.path().join("orig"), &tmp.path().join("parent/copy")).unwrap();
|
||||
assert!(tmp.path().join("parent/copy").is_dir());
|
||||
assert!(tmp.path().join("parent/copy/sub").is_dir());
|
||||
assert!(tmp.path().join("parent/copy/sub/file.txt").is_file());
|
||||
assert_eq!(
|
||||
std::fs::read(tmp.path().join("parent/copy/sub/file.txt"))
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
b"Hello, world!\n"
|
||||
);
|
||||
assert!(tmp.path().join("parent/copy/link").exists());
|
||||
assert_eq!(
|
||||
std::fs::read_link(tmp.path().join("parent/copy/link")).unwrap(),
|
||||
PathBuf::from("sub/file.txt")
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(tmp.path().join("parent/copy/link"))
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
b"Hello, world!\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retina_icon_paths() {
|
||||
assert!(!is_retina("data/icons/512x512.png"));
|
||||
assert!(is_retina("data/icons/512x512@2x.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_relative_paths() {
|
||||
assert_eq!(
|
||||
resource_relpath(&PathBuf::from("./data/images/button.png")),
|
||||
PathBuf::from("data/images/button.png")
|
||||
);
|
||||
assert_eq!(
|
||||
resource_relpath(&PathBuf::from("../../images/wheel.png")),
|
||||
PathBuf::from("_up_/_up_/images/wheel.png")
|
||||
);
|
||||
assert_eq!(
|
||||
resource_relpath(&PathBuf::from("/home/ferris/crab.png")),
|
||||
PathBuf::from("_root_/home/ferris/crab.png")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
// The structure of a Debian package looks something like this:
|
||||
//
|
||||
// foobar_1.2.3_i386.deb # Actually an ar archive
|
||||
// debian-binary # Specifies deb format version (2.0 in our case)
|
||||
// control.tar.gz # Contains files controlling the installation:
|
||||
// control # Basic package metadata
|
||||
// md5sums # Checksums for files in data.tar.gz below
|
||||
// postinst # Post-installation script (optional)
|
||||
// prerm # Pre-uninstallation script (optional)
|
||||
// data.tar.gz # Contains files to be installed:
|
||||
// usr/bin/foobar # Binary executable file
|
||||
// usr/share/applications/foobar.desktop # Desktop file (for apps)
|
||||
// usr/share/icons/hicolor/... # Icon files (for apps)
|
||||
// usr/lib/foobar/... # Other resource files
|
||||
//
|
||||
// For cargo-bundle, we put bundle resource files under /usr/lib/package_name/,
|
||||
// and then generate the desktop file and control file from the bundle
|
||||
// metadata, as well as generating the md5sums file. Currently we do not
|
||||
// generate postinst or prerm files.
|
||||
|
||||
use super::common;
|
||||
use ar;
|
||||
use icns;
|
||||
use image::png::{PNGDecoder, PNGEncoder};
|
||||
use image::{self, GenericImage, ImageDecoder};
|
||||
use libflate::gzip;
|
||||
use md5;
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tar;
|
||||
use walkdir::WalkDir;
|
||||
use crate::{ResultExt, Settings};
|
||||
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let arch = match settings.binary_arch() {
|
||||
"x86" => "i386",
|
||||
"x86_64" => "amd64",
|
||||
other => other,
|
||||
};
|
||||
let package_base_name = format!(
|
||||
"{}_{}_{}",
|
||||
settings.binary_name(),
|
||||
settings.version_string(),
|
||||
arch
|
||||
);
|
||||
let package_name = format!("{}.deb", package_base_name);
|
||||
common::print_bundling(&package_name)?;
|
||||
let base_dir = settings.project_out_directory().join("bundle/deb");
|
||||
let package_dir = base_dir.join(&package_base_name);
|
||||
if package_dir.exists() {
|
||||
fs::remove_dir_all(&package_dir)
|
||||
.chain_err(|| format!("Failed to remove old {}", package_base_name))?;
|
||||
}
|
||||
let package_path = base_dir.join(package_name);
|
||||
|
||||
// Generate data files.
|
||||
let data_dir = package_dir.join("data");
|
||||
let binary_dest = data_dir.join("usr/bin").join(settings.binary_name());
|
||||
common::copy_file(settings.binary_path(), &binary_dest)
|
||||
.chain_err(|| "Failed to copy binary file")?;
|
||||
transfer_resource_files(settings, &data_dir).chain_err(|| "Failed to copy resource files")?;
|
||||
generate_icon_files(settings, &data_dir).chain_err(|| "Failed to create icon files")?;
|
||||
generate_desktop_file(settings, &data_dir).chain_err(|| "Failed to create desktop file")?;
|
||||
|
||||
// Generate control files.
|
||||
let control_dir = package_dir.join("control");
|
||||
generate_control_file(settings, arch, &control_dir, &data_dir)
|
||||
.chain_err(|| "Failed to create control file")?;
|
||||
generate_md5sums(&control_dir, &data_dir).chain_err(|| "Failed to create md5sums file")?;
|
||||
|
||||
// Generate `debian-binary` file; see
|
||||
// http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66
|
||||
let debian_binary_path = package_dir.join("debian-binary");
|
||||
create_file_with_data(&debian_binary_path, "2.0\n")
|
||||
.chain_err(|| "Failed to create debian-binary file")?;
|
||||
|
||||
// Apply tar/gzip/ar to create the final package file.
|
||||
let control_tar_gz_path =
|
||||
tar_and_gzip_dir(control_dir).chain_err(|| "Failed to tar/gzip control directory")?;
|
||||
let data_tar_gz_path =
|
||||
tar_and_gzip_dir(data_dir).chain_err(|| "Failed to tar/gzip data directory")?;
|
||||
create_archive(
|
||||
vec![debian_binary_path, control_tar_gz_path, data_tar_gz_path],
|
||||
&package_path,
|
||||
)
|
||||
.chain_err(|| "Failed to create package archive")?;
|
||||
Ok(vec![package_path])
|
||||
}
|
||||
|
||||
/// Generate the application desktop file and store it under the `data_dir`.
|
||||
fn generate_desktop_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
|
||||
let bin_name = settings.binary_name();
|
||||
let desktop_file_name = format!("{}.desktop", bin_name);
|
||||
let desktop_file_path = data_dir
|
||||
.join("usr/share/applications")
|
||||
.join(desktop_file_name);
|
||||
let file = &mut common::create_file(&desktop_file_path)?;
|
||||
// For more information about the format of this file, see
|
||||
// https://developer.gnome.org/integration-guide/stable/desktop-files.html.en
|
||||
write!(file, "[Desktop Entry]\n")?;
|
||||
write!(file, "Encoding=UTF-8\n")?;
|
||||
if let Some(category) = settings.app_category() {
|
||||
write!(file, "Categories={}\n", category.gnome_desktop_categories())?;
|
||||
}
|
||||
if !settings.short_description().is_empty() {
|
||||
write!(file, "Comment={}\n", settings.short_description())?;
|
||||
}
|
||||
write!(file, "Exec={}\n", bin_name)?;
|
||||
write!(file, "Icon={}\n", bin_name)?;
|
||||
write!(file, "Name={}\n", settings.bundle_name())?;
|
||||
write!(file, "Terminal=false\n")?;
|
||||
write!(file, "Type=Application\n")?;
|
||||
write!(file, "Version={}\n", settings.version_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_control_file(
|
||||
settings: &Settings,
|
||||
arch: &str,
|
||||
control_dir: &Path,
|
||||
data_dir: &Path,
|
||||
) -> crate::Result<()> {
|
||||
// For more information about the format of this file, see
|
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html
|
||||
let dest_path = control_dir.join("control");
|
||||
let mut file = common::create_file(&dest_path)?;
|
||||
writeln!(
|
||||
&mut file,
|
||||
"Package: {}",
|
||||
str::replace(settings.bundle_name(), " ", "-").to_ascii_lowercase()
|
||||
)?;
|
||||
writeln!(&mut file, "Version: {}", settings.version_string())?;
|
||||
writeln!(&mut file, "Architecture: {}", arch)?;
|
||||
writeln!(&mut file, "Installed-Size: {}", total_dir_size(data_dir)?)?;
|
||||
let authors = settings.authors_comma_separated().unwrap_or(String::new());
|
||||
writeln!(&mut file, "Maintainer: {}", authors)?;
|
||||
if !settings.homepage_url().is_empty() {
|
||||
writeln!(&mut file, "Homepage: {}", settings.homepage_url())?;
|
||||
}
|
||||
let dependencies = settings.debian_dependencies();
|
||||
if !dependencies.is_empty() {
|
||||
writeln!(&mut file, "Depends: {}", dependencies.join(", "))?;
|
||||
}
|
||||
let mut short_description = settings.short_description().trim();
|
||||
if short_description.is_empty() {
|
||||
short_description = "(none)";
|
||||
}
|
||||
let mut long_description = settings.long_description().unwrap_or("").trim();
|
||||
if long_description.is_empty() {
|
||||
long_description = "(none)";
|
||||
}
|
||||
writeln!(&mut file, "Description: {}", short_description)?;
|
||||
for line in long_description.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
writeln!(&mut file, " .")?;
|
||||
} else {
|
||||
writeln!(&mut file, " {}", line)?;
|
||||
}
|
||||
}
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an `md5sums` file in the `control_dir` containing the MD5 checksums
|
||||
/// for each file within the `data_dir`.
|
||||
fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> {
|
||||
let md5sums_path = control_dir.join("md5sums");
|
||||
let mut md5sums_file = common::create_file(&md5sums_path)?;
|
||||
for entry in WalkDir::new(data_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let mut file = File::open(path)?;
|
||||
let mut hash = md5::Context::new();
|
||||
io::copy(&mut file, &mut hash)?;
|
||||
for byte in hash.compute().iter() {
|
||||
write!(md5sums_file, "{:02x}", byte)?;
|
||||
}
|
||||
let rel_path = path.strip_prefix(data_dir).unwrap();
|
||||
let path_str = rel_path.to_str().ok_or_else(|| {
|
||||
let msg = format!("Non-UTF-8 path: {:?}", rel_path);
|
||||
io::Error::new(io::ErrorKind::InvalidData, msg)
|
||||
})?;
|
||||
write!(md5sums_file, " {}\n", path_str)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy the bundle's resource files into an appropriate directory under the
|
||||
/// `data_dir`.
|
||||
fn transfer_resource_files(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
|
||||
let resource_dir = data_dir.join("usr/lib").join(settings.binary_name());
|
||||
for src in settings.resource_files() {
|
||||
let src = src?;
|
||||
let dest = resource_dir.join(common::resource_relpath(&src));
|
||||
common::copy_file(&src, &dest)
|
||||
.chain_err(|| format!("Failed to copy resource file {:?}", src))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate the icon files and store them under the `data_dir`.
|
||||
fn generate_icon_files(settings: &Settings, data_dir: &PathBuf) -> crate::Result<()> {
|
||||
let base_dir = data_dir.join("usr/share/icons/hicolor");
|
||||
let get_dest_path = |width: u32, height: u32, is_high_density: bool| {
|
||||
base_dir.join(format!(
|
||||
"{}x{}{}/apps/{}.png",
|
||||
width,
|
||||
height,
|
||||
if is_high_density { "@2x" } else { "" },
|
||||
settings.binary_name()
|
||||
))
|
||||
};
|
||||
let mut sizes = BTreeSet::new();
|
||||
// Prefer PNG files.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() != Some(OsStr::new("png")) {
|
||||
continue;
|
||||
}
|
||||
let mut decoder = PNGDecoder::new(File::open(&icon_path)?);
|
||||
let (width, height) = decoder.dimensions()?;
|
||||
let is_high_density = common::is_retina(&icon_path);
|
||||
if !sizes.contains(&(width, height, is_high_density)) {
|
||||
sizes.insert((width, height, is_high_density));
|
||||
let dest_path = get_dest_path(width, height, is_high_density);
|
||||
common::copy_file(&icon_path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
// Fall back to non-PNG files for any missing sizes.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() == Some(OsStr::new("png")) {
|
||||
continue;
|
||||
} else if icon_path.extension() == Some(OsStr::new("icns")) {
|
||||
let icon_family = icns::IconFamily::read(File::open(&icon_path)?)?;
|
||||
for icon_type in icon_family.available_icons() {
|
||||
let width = icon_type.screen_width();
|
||||
let height = icon_type.screen_height();
|
||||
let is_high_density = icon_type.pixel_density() > 1;
|
||||
if !sizes.contains(&(width, height, is_high_density)) {
|
||||
sizes.insert((width, height, is_high_density));
|
||||
let dest_path = get_dest_path(width, height, is_high_density);
|
||||
let icon = icon_family.get_icon_with_type(icon_type)?;
|
||||
icon.write_png(common::create_file(&dest_path)?)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let icon = r#try!(image::open(&icon_path));
|
||||
let (width, height) = icon.dimensions();
|
||||
let is_high_density = common::is_retina(&icon_path);
|
||||
if !sizes.contains(&(width, height, is_high_density)) {
|
||||
sizes.insert((width, height, is_high_density));
|
||||
let dest_path = get_dest_path(width, height, is_high_density);
|
||||
let encoder = PNGEncoder::new(common::create_file(&dest_path)?);
|
||||
encoder.encode(&icon.raw_pixels(), width, height, icon.color())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an empty file at the given path, creating any parent directories as
|
||||
/// needed, then write `data` into the file.
|
||||
fn create_file_with_data<P: AsRef<Path>>(path: P, data: &str) -> crate::Result<()> {
|
||||
let mut file = common::create_file(path.as_ref())?;
|
||||
file.write_all(data.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Computes the total size, in bytes, of the given directory and all of its
|
||||
/// contents.
|
||||
fn total_dir_size(dir: &Path) -> crate::Result<u64> {
|
||||
let mut total: u64 = 0;
|
||||
for entry in WalkDir::new(&dir) {
|
||||
total += entry?.metadata()?.len();
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
/// Writes a tar file to the given writer containing the given directory.
|
||||
fn create_tar_from_dir<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let mut tar_builder = tar::Builder::new(dest_file);
|
||||
for entry in WalkDir::new(&src_dir) {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
if src_path == src_dir {
|
||||
continue;
|
||||
}
|
||||
let dest_path = src_path.strip_prefix(&src_dir).unwrap();
|
||||
if entry.file_type().is_dir() {
|
||||
tar_builder.append_dir(dest_path, src_path)?;
|
||||
} else {
|
||||
let mut src_file = fs::File::open(src_path)?;
|
||||
tar_builder.append_file(dest_path, &mut src_file)?;
|
||||
}
|
||||
}
|
||||
let dest_file = tar_builder.into_inner()?;
|
||||
Ok(dest_file)
|
||||
}
|
||||
|
||||
/// Creates a `.tar.gz` file from the given directory (placing the new file
|
||||
/// within the given directory's parent directory), then deletes the original
|
||||
/// directory and returns the path to the new file.
|
||||
fn tar_and_gzip_dir<P: AsRef<Path>>(src_dir: P) -> crate::Result<PathBuf> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let dest_path = src_dir.with_extension("tar.gz");
|
||||
let dest_file = common::create_file(&dest_path)?;
|
||||
let gzip_encoder = gzip::Encoder::new(dest_file)?;
|
||||
let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?;
|
||||
let mut dest_file = gzip_encoder.finish().into_result()?;
|
||||
dest_file.flush()?;
|
||||
Ok(dest_path)
|
||||
}
|
||||
|
||||
/// Creates an `ar` archive from the given source files and writes it to the
|
||||
/// given destination path.
|
||||
fn create_archive(srcs: Vec<PathBuf>, dest: &Path) -> crate::Result<()> {
|
||||
let mut builder = ar::Builder::new(common::create_file(&dest)?);
|
||||
for path in &srcs {
|
||||
builder.append_path(path)?;
|
||||
}
|
||||
builder.into_inner()?.flush()?;
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
// An iOS package is laid out like:
|
||||
//
|
||||
// Foobar.app # Actually a directory
|
||||
// Foobar # The main binary executable of the app
|
||||
// Info.plist # An XML file containing the app's metadata
|
||||
// ... # Icons and other resource files
|
||||
//
|
||||
// See https://developer.apple.com/go/?id=bundle-structure for a full
|
||||
// explanation.
|
||||
|
||||
use super::common;
|
||||
use icns;
|
||||
use image::png::{PNGDecoder, PNGEncoder};
|
||||
use image::{self, GenericImage, ImageDecoder};
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::{ResultExt, Settings};
|
||||
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
common::print_warning("iOS bundle support is still experimental.")?;
|
||||
|
||||
let app_bundle_name = format!("{}.app", settings.bundle_name());
|
||||
common::print_bundling(&app_bundle_name)?;
|
||||
let bundle_dir = settings
|
||||
.project_out_directory()
|
||||
.join("bundle/ios")
|
||||
.join(&app_bundle_name);
|
||||
if bundle_dir.exists() {
|
||||
fs::remove_dir_all(&bundle_dir)
|
||||
.chain_err(|| format!("Failed to remove old {}", app_bundle_name))?;
|
||||
}
|
||||
fs::create_dir_all(&bundle_dir)
|
||||
.chain_err(|| format!("Failed to create bundle directory at {:?}", bundle_dir))?;
|
||||
|
||||
for src in settings.resource_files() {
|
||||
let src = src?;
|
||||
let dest = bundle_dir.join(common::resource_relpath(&src));
|
||||
common::copy_file(&src, &dest)
|
||||
.chain_err(|| format!("Failed to copy resource file {:?}", src))?;
|
||||
}
|
||||
|
||||
let icon_filenames =
|
||||
generate_icon_files(&bundle_dir, settings).chain_err(|| "Failed to create app icons")?;
|
||||
generate_info_plist(&bundle_dir, settings, &icon_filenames)
|
||||
.chain_err(|| "Failed to create Info.plist")?;
|
||||
let bin_path = bundle_dir.join(&settings.bundle_name());
|
||||
common::copy_file(settings.binary_path(), &bin_path)
|
||||
.chain_err(|| format!("Failed to copy binary from {:?}", settings.binary_path()))?;
|
||||
Ok(vec![bundle_dir])
|
||||
}
|
||||
|
||||
/// Generate the icon files and store them under the `bundle_dir`.
|
||||
fn generate_icon_files(bundle_dir: &Path, settings: &Settings) -> crate::Result<Vec<String>> {
|
||||
let mut filenames = Vec::new();
|
||||
{
|
||||
let mut get_dest_path = |width: u32, height: u32, is_retina: bool| {
|
||||
let filename = format!(
|
||||
"icon_{}x{}{}.png",
|
||||
width,
|
||||
height,
|
||||
if is_retina { "@2x" } else { "" }
|
||||
);
|
||||
let path = bundle_dir.join(&filename);
|
||||
filenames.push(filename);
|
||||
path
|
||||
};
|
||||
let mut sizes = BTreeSet::new();
|
||||
// Prefer PNG files.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() != Some(OsStr::new("png")) {
|
||||
continue;
|
||||
}
|
||||
let mut decoder = PNGDecoder::new(File::open(&icon_path)?);
|
||||
let (width, height) = decoder.dimensions()?;
|
||||
let is_retina = common::is_retina(&icon_path);
|
||||
if !sizes.contains(&(width, height, is_retina)) {
|
||||
sizes.insert((width, height, is_retina));
|
||||
let dest_path = get_dest_path(width, height, is_retina);
|
||||
common::copy_file(&icon_path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
// Fall back to non-PNG files for any missing sizes.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() == Some(OsStr::new("png")) {
|
||||
continue;
|
||||
} else if icon_path.extension() == Some(OsStr::new("icns")) {
|
||||
let icon_family = icns::IconFamily::read(File::open(&icon_path)?)?;
|
||||
for icon_type in icon_family.available_icons() {
|
||||
let width = icon_type.screen_width();
|
||||
let height = icon_type.screen_height();
|
||||
let is_retina = icon_type.pixel_density() > 1;
|
||||
if !sizes.contains(&(width, height, is_retina)) {
|
||||
sizes.insert((width, height, is_retina));
|
||||
let dest_path = get_dest_path(width, height, is_retina);
|
||||
let icon = icon_family.get_icon_with_type(icon_type)?;
|
||||
icon.write_png(File::create(dest_path)?)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let icon = r#try!(image::open(&icon_path));
|
||||
let (width, height) = icon.dimensions();
|
||||
let is_retina = common::is_retina(&icon_path);
|
||||
if !sizes.contains(&(width, height, is_retina)) {
|
||||
sizes.insert((width, height, is_retina));
|
||||
let dest_path = get_dest_path(width, height, is_retina);
|
||||
let encoder = PNGEncoder::new(common::create_file(&dest_path)?);
|
||||
encoder.encode(&icon.raw_pixels(), width, height, icon.color())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(filenames)
|
||||
}
|
||||
|
||||
fn generate_info_plist(
|
||||
bundle_dir: &Path,
|
||||
settings: &Settings,
|
||||
icon_filenames: &Vec<String>,
|
||||
) -> crate::Result<()> {
|
||||
let file = &mut common::create_file(&bundle_dir.join("Info.plist"))?;
|
||||
write!(
|
||||
file,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \
|
||||
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
|
||||
<plist version=\"1.0\">\n\
|
||||
<dict>\n"
|
||||
)?;
|
||||
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleIdentifier</key>\n <string>{}</string>\n",
|
||||
settings.bundle_identifier()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleDisplayName</key>\n <string>{}</string>\n",
|
||||
settings.bundle_name()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleName</key>\n <string>{}</string>\n",
|
||||
settings.bundle_name()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleExecutable</key>\n <string>{}</string>\n",
|
||||
settings.binary_name()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleVersion</key>\n <string>{}</string>\n",
|
||||
settings.version_string()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleDevelopmentRegion</key>\n <string>en_US</string>\n"
|
||||
)?;
|
||||
|
||||
if !icon_filenames.is_empty() {
|
||||
write!(file, " <key>CFBundleIconFiles</key>\n <array>\n")?;
|
||||
for filename in icon_filenames {
|
||||
write!(file, " <string>{}</string>\n", filename)?;
|
||||
}
|
||||
write!(file, " </array>\n")?;
|
||||
}
|
||||
write!(file, " <key>LSRequiresIPhoneOS</key>\n <true/>\n")?;
|
||||
write!(file, "</dict>\n</plist>\n")?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
mod category;
|
||||
mod common;
|
||||
mod deb_bundle;
|
||||
mod ios_bundle;
|
||||
mod msi_bundle;
|
||||
mod osx_bundle;
|
||||
mod rpm_bundle;
|
||||
mod settings;
|
||||
|
||||
pub use self::common::{print_error, print_finished};
|
||||
pub use self::settings::{BuildArtifact, PackageType, Settings};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn bundle_project(settings: Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let mut paths = Vec::new();
|
||||
for package_type in settings.package_types()? {
|
||||
paths.append(&mut match package_type {
|
||||
PackageType::OsxBundle => osx_bundle::bundle_project(&settings)?,
|
||||
PackageType::IosBundle => ios_bundle::bundle_project(&settings)?,
|
||||
PackageType::WindowsMsi => msi_bundle::bundle_project(&settings)?,
|
||||
PackageType::Deb => deb_bundle::bundle_project(&settings)?,
|
||||
PackageType::Rpm => rpm_bundle::bundle_project(&settings)?,
|
||||
});
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
|
@ -0,0 +1,614 @@
|
|||
use super::common;
|
||||
use super::settings::Settings;
|
||||
use cab;
|
||||
use msi;
|
||||
use std;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use uuid::Uuid;
|
||||
use crate::ResultExt;
|
||||
|
||||
type Package = msi::Package<fs::File>;
|
||||
|
||||
// Don't add more files to a cabinet folder that already has this many bytes:
|
||||
const CABINET_FOLDER_SIZE_LIMIT: u64 = 0x8000;
|
||||
// The maximum number of resource files we'll put in one cabinet:
|
||||
const CABINET_MAX_FILES: usize = 1000;
|
||||
// The maximum number of data bytes we'll put in one cabinet:
|
||||
const CABINET_MAX_SIZE: u64 = 0x1000_0000;
|
||||
|
||||
// File table attribute indicating that a file is "vital":
|
||||
const FILE_ATTR_VITAL: u16 = 0x200;
|
||||
|
||||
// The name of the installer package's sole Feature:
|
||||
const MAIN_FEATURE_NAME: &str = "MainFeature";
|
||||
|
||||
// A v4 UUID that was generated specifically for cargo-bundle, to be used as a
|
||||
// namespace for generating v5 UUIDs from bundle identifier strings.
|
||||
const UUID_NAMESPACE: [u8; 16] = [
|
||||
0xfd, 0x85, 0x95, 0xa8, 0x17, 0xa3, 0x47, 0x4e, 0xa6, 0x16, 0x76, 0x14, 0x8d, 0xfa, 0x0c, 0x7b,
|
||||
];
|
||||
|
||||
// Info about a resource file (including the main executable) in the bundle.
|
||||
struct ResourceInfo {
|
||||
// The path to the existing file that will be bundled as a resource.
|
||||
source_path: PathBuf,
|
||||
// Relative path from the install dir where this will be installed.
|
||||
dest_path: PathBuf,
|
||||
// The name of this resource file in the filesystem.
|
||||
filename: String,
|
||||
// The size of this resource file, in bytes.
|
||||
size: u64,
|
||||
// The database key for the Component that this resource is part of.
|
||||
component_key: String,
|
||||
}
|
||||
|
||||
// Info about a directory that needs to be created during installation.
|
||||
struct DirectoryInfo {
|
||||
// The database key for this directory.
|
||||
key: String,
|
||||
// The database key for this directory's parent.
|
||||
parent_key: String,
|
||||
// The name of this directory in the filesystem.
|
||||
name: String,
|
||||
// List of files in this directory, not counting subdirectories.
|
||||
files: Vec<String>,
|
||||
}
|
||||
|
||||
// Info about a CAB archive within the installer package.
|
||||
struct CabinetInfo {
|
||||
// The stream name for this cabinet.
|
||||
name: String,
|
||||
// The resource files that are in this cabinet.
|
||||
resources: Vec<ResourceInfo>,
|
||||
}
|
||||
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
common::print_warning("MSI bundle support is still experimental.")?;
|
||||
|
||||
let msi_name = format!("{}.msi", settings.bundle_name());
|
||||
common::print_bundling(&msi_name)?;
|
||||
let base_dir = settings.project_out_directory().join("bundle/msi");
|
||||
let msi_path = base_dir.join(&msi_name);
|
||||
let mut package =
|
||||
new_empty_package(&msi_path).chain_err(|| "Failed to initialize MSI package")?;
|
||||
|
||||
// Generate package metadata:
|
||||
let guid = generate_package_guid(settings);
|
||||
set_summary_info(&mut package, guid, settings);
|
||||
create_property_table(&mut package, guid, settings)
|
||||
.chain_err(|| "Failed to generate Property table")?;
|
||||
|
||||
// Copy resource files into package:
|
||||
let mut resources =
|
||||
collect_resource_info(settings).chain_err(|| "Failed to collect resource file information")?;
|
||||
let directories = collect_directory_info(settings, &mut resources)
|
||||
.chain_err(|| "Failed to collect resource directory information")?;
|
||||
let cabinets = divide_resources_into_cabinets(resources);
|
||||
generate_resource_cabinets(&mut package, &cabinets)
|
||||
.chain_err(|| "Failed to generate resource cabinets")?;
|
||||
|
||||
// Set up installer database tables:
|
||||
create_directory_table(&mut package, &directories)
|
||||
.chain_err(|| "Failed to generate Directory table")?;
|
||||
create_feature_table(&mut package, settings).chain_err(|| "Failed to generate Feature table")?;
|
||||
create_component_table(&mut package, guid, &directories)
|
||||
.chain_err(|| "Failed to generate Component table")?;
|
||||
create_feature_components_table(&mut package, &directories)
|
||||
.chain_err(|| "Failed to generate FeatureComponents table")?;
|
||||
create_media_table(&mut package, &cabinets).chain_err(|| "Failed to generate Media table")?;
|
||||
create_file_table(&mut package, &cabinets).chain_err(|| "Failed to generate File table")?;
|
||||
// TODO: Create other needed tables.
|
||||
|
||||
// Create app icon:
|
||||
package.create_table(
|
||||
"Icon",
|
||||
vec![
|
||||
msi::Column::build("Name").primary_key().id_string(72),
|
||||
msi::Column::build("Data").binary(),
|
||||
],
|
||||
)?;
|
||||
let icon_name = format!("{}.ico", settings.binary_name());
|
||||
{
|
||||
let stream_name = format!("Icon.{}", icon_name);
|
||||
let mut stream = package.write_stream(&stream_name)?;
|
||||
create_app_icon(&mut stream, settings)?;
|
||||
}
|
||||
package.insert_rows(msi::Insert::into("Icon").row(vec![
|
||||
msi::Value::Str(icon_name.clone()),
|
||||
msi::Value::from("Name"),
|
||||
]))?;
|
||||
|
||||
package.flush()?;
|
||||
Ok(vec![msi_path])
|
||||
}
|
||||
|
||||
fn new_empty_package(msi_path: &Path) -> crate::Result<Package> {
|
||||
if let Some(parent) = msi_path.parent() {
|
||||
fs::create_dir_all(&parent).chain_err(|| format!("Failed to create directory {:?}", parent))?;
|
||||
}
|
||||
let msi_file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(msi_path)
|
||||
.chain_err(|| format!("Failed to create file {:?}", msi_path))?;
|
||||
let package = msi::Package::create(msi::PackageType::Installer, msi_file)?;
|
||||
Ok(package)
|
||||
}
|
||||
|
||||
// Generates a GUID for the package, based on `settings.bundle_identifier()`.
|
||||
fn generate_package_guid(settings: &Settings) -> Uuid {
|
||||
let namespace = Uuid::from_bytes(&UUID_NAMESPACE).unwrap();
|
||||
Uuid::new_v5(&namespace, &settings.bundle_identifier())
|
||||
}
|
||||
|
||||
// Populates the summary metadata for the package from the bundle settings.
|
||||
fn set_summary_info(package: &mut Package, package_guid: Uuid, settings: &Settings) {
|
||||
let summary_info = package.summary_info_mut();
|
||||
summary_info.set_creation_time_to_now();
|
||||
summary_info.set_subject(settings.bundle_name().to_string());
|
||||
summary_info.set_uuid(package_guid);
|
||||
summary_info.set_comments(settings.short_description().to_string());
|
||||
if let Some(authors) = settings.authors_comma_separated() {
|
||||
summary_info.set_author(authors);
|
||||
}
|
||||
let creating_app = format!("cargo-bundle v{}", crate_version!());
|
||||
summary_info.set_creating_application(creating_app);
|
||||
}
|
||||
|
||||
// Creates and populates the `Property` database table for the package.
|
||||
fn create_property_table(
|
||||
package: &mut Package,
|
||||
package_guid: Uuid,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<()> {
|
||||
let authors = settings.authors_comma_separated().unwrap_or(String::new());
|
||||
package.create_table(
|
||||
"Property",
|
||||
vec![
|
||||
msi::Column::build("Property").primary_key().id_string(72),
|
||||
msi::Column::build("Value").text_string(0),
|
||||
],
|
||||
)?;
|
||||
package.insert_rows(
|
||||
msi::Insert::into("Property")
|
||||
.row(vec![
|
||||
msi::Value::from("Manufacturer"),
|
||||
msi::Value::Str(authors),
|
||||
])
|
||||
.row(vec![
|
||||
msi::Value::from("ProductCode"),
|
||||
msi::Value::from(package_guid),
|
||||
])
|
||||
.row(vec![
|
||||
msi::Value::from("ProductLanguage"),
|
||||
msi::Value::from(msi::Language::from_tag("en-US")),
|
||||
])
|
||||
.row(vec![
|
||||
msi::Value::from("ProductName"),
|
||||
msi::Value::from(settings.bundle_name()),
|
||||
])
|
||||
.row(vec![
|
||||
msi::Value::from("ProductVersion"),
|
||||
msi::Value::from(settings.version_string()),
|
||||
]),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Returns a list of `ResourceInfo` structs for the binary executable and all
|
||||
// the resource files that should be included in the package.
|
||||
fn collect_resource_info(settings: &Settings) -> crate::Result<Vec<ResourceInfo>> {
|
||||
let mut resources = Vec::<ResourceInfo>::new();
|
||||
resources.push(ResourceInfo {
|
||||
source_path: settings.binary_path().to_path_buf(),
|
||||
dest_path: PathBuf::from(settings.binary_name()),
|
||||
filename: settings.binary_name().to_string(),
|
||||
size: settings.binary_path().metadata()?.len(),
|
||||
component_key: String::new(),
|
||||
});
|
||||
let root_rsrc_dir = PathBuf::from("Resources");
|
||||
for source_path in settings.resource_files() {
|
||||
let source_path = source_path?;
|
||||
let metadata = source_path.metadata()?;
|
||||
let size = metadata.len();
|
||||
let dest_path = root_rsrc_dir.join(common::resource_relpath(&source_path));
|
||||
let filename = dest_path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let info = ResourceInfo {
|
||||
source_path,
|
||||
dest_path,
|
||||
filename,
|
||||
size,
|
||||
component_key: String::new(),
|
||||
};
|
||||
resources.push(info);
|
||||
}
|
||||
Ok(resources)
|
||||
}
|
||||
|
||||
// Based on the list of all resource files to be bundled, returns a list of
|
||||
// all the directories that need to be created during installation. Also,
|
||||
// modifies each `ResourceInfo` object to populate its `component_key` field
|
||||
// with the database key of the Component that the resource will be associated
|
||||
// with.
|
||||
fn collect_directory_info(
|
||||
settings: &Settings,
|
||||
resources: &mut Vec<ResourceInfo>,
|
||||
) -> crate::Result<Vec<DirectoryInfo>> {
|
||||
let mut dir_map = BTreeMap::<PathBuf, DirectoryInfo>::new();
|
||||
let mut dir_index: i32 = 0;
|
||||
dir_map.insert(
|
||||
PathBuf::new(),
|
||||
DirectoryInfo {
|
||||
key: "INSTALLDIR".to_string(),
|
||||
parent_key: "ProgramFilesFolder".to_string(),
|
||||
name: settings.bundle_name().to_string(),
|
||||
files: Vec::new(),
|
||||
},
|
||||
);
|
||||
for resource in resources.iter_mut() {
|
||||
let mut dir_key = "INSTALLDIR".to_string();
|
||||
let mut dir_path = PathBuf::new();
|
||||
for component in resource.dest_path.parent().unwrap().components() {
|
||||
if let std::path::Component::Normal(name) = component {
|
||||
dir_path.push(name);
|
||||
if dir_map.contains_key(&dir_path) {
|
||||
dir_key = dir_map.get(&dir_path).unwrap().key.clone();
|
||||
} else {
|
||||
let new_key = format!("RDIR{:04}", dir_index);
|
||||
dir_map.insert(
|
||||
dir_path.clone(),
|
||||
DirectoryInfo {
|
||||
key: new_key.clone(),
|
||||
parent_key: dir_key.clone(),
|
||||
name: name.to_string_lossy().to_string(),
|
||||
files: Vec::new(),
|
||||
},
|
||||
);
|
||||
dir_key = new_key;
|
||||
dir_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let directory = dir_map.get_mut(&dir_path).unwrap();
|
||||
debug_assert_eq!(directory.key, dir_key);
|
||||
directory.files.push(resource.filename.clone());
|
||||
resource.component_key = dir_key.to_string();
|
||||
}
|
||||
Ok(dir_map.into_iter().map(|(_k, v)| v).collect())
|
||||
}
|
||||
|
||||
// Divides up the list of resource into some number of cabinets, subject to a
|
||||
// few contraints: 1) no one cabinet will have two resources with the same
|
||||
// filename, 2) no one cabinet will have more than `CABINET_MAX_FILES` files
|
||||
// in it, and 3) no one cabinet will contain mroe than `CABINET_MAX_SIZE`
|
||||
// bytes of data (unless that cabinet consists of a single file that is
|
||||
// already bigger than that).
|
||||
fn divide_resources_into_cabinets(mut resources: Vec<ResourceInfo>) -> Vec<CabinetInfo> {
|
||||
let mut cabinets = Vec::new();
|
||||
while !resources.is_empty() {
|
||||
let mut filenames = HashSet::<String>::new();
|
||||
let mut total_size = 0;
|
||||
let mut leftovers = Vec::<ResourceInfo>::new();
|
||||
let mut cabinet = CabinetInfo {
|
||||
name: format!("rsrc{:04}.cab", cabinets.len()),
|
||||
resources: Vec::new(),
|
||||
};
|
||||
for resource in resources.into_iter() {
|
||||
if cabinet.resources.len() >= CABINET_MAX_FILES
|
||||
|| (!cabinet.resources.is_empty() && total_size + resource.size > CABINET_MAX_SIZE)
|
||||
|| filenames.contains(&resource.filename)
|
||||
{
|
||||
leftovers.push(resource);
|
||||
} else {
|
||||
filenames.insert(resource.filename.clone());
|
||||
total_size += resource.size;
|
||||
cabinet.resources.push(resource);
|
||||
}
|
||||
}
|
||||
cabinets.push(cabinet);
|
||||
resources = leftovers;
|
||||
}
|
||||
cabinets
|
||||
}
|
||||
|
||||
// Creates the CAB archives within the package that contain the binary
|
||||
// execuable and all the resource files.
|
||||
fn generate_resource_cabinets(package: &mut Package, cabinets: &[CabinetInfo]) -> crate::Result<()> {
|
||||
for cabinet_info in cabinets.iter() {
|
||||
let mut builder = cab::CabinetBuilder::new();
|
||||
let mut file_map = HashMap::<String, &Path>::new();
|
||||
let mut resource_index: usize = 0;
|
||||
while resource_index < cabinet_info.resources.len() {
|
||||
let folder = builder.add_folder(cab::CompressionType::MsZip);
|
||||
let mut folder_size: u64 = 0;
|
||||
while resource_index < cabinet_info.resources.len() && folder_size < CABINET_FOLDER_SIZE_LIMIT
|
||||
{
|
||||
let resource = &cabinet_info.resources[resource_index];
|
||||
folder_size += resource.size;
|
||||
folder.add_file(resource.filename.as_str());
|
||||
debug_assert!(!file_map.contains_key(&resource.filename));
|
||||
file_map.insert(resource.filename.clone(), &resource.source_path);
|
||||
resource_index += 1;
|
||||
}
|
||||
}
|
||||
let stream = package.write_stream(cabinet_info.name.as_str())?;
|
||||
let mut cabinet_writer = builder.build(stream)?;
|
||||
while let Some(mut file_writer) = cabinet_writer.next_file()? {
|
||||
debug_assert!(file_map.contains_key(file_writer.file_name()));
|
||||
let file_path = file_map.get(file_writer.file_name()).unwrap();
|
||||
let mut file = fs::File::open(file_path)?;
|
||||
io::copy(&mut file, &mut file_writer)?;
|
||||
}
|
||||
cabinet_writer.finish()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Creates and populates the `Directory` database table for the package.
|
||||
fn create_directory_table(package: &mut Package, directories: &[DirectoryInfo]) -> crate::Result<()> {
|
||||
package.create_table(
|
||||
"Directory",
|
||||
vec![
|
||||
msi::Column::build("Directory").primary_key().id_string(72),
|
||||
msi::Column::build("Directory_Parent")
|
||||
.nullable()
|
||||
.foreign_key("Directory", 1)
|
||||
.id_string(72),
|
||||
msi::Column::build("DefaultDir")
|
||||
.category(msi::Category::DefaultDir)
|
||||
.string(255),
|
||||
],
|
||||
)?;
|
||||
let mut rows = Vec::new();
|
||||
for directory in directories.iter() {
|
||||
rows.push(vec![
|
||||
msi::Value::Str(directory.key.clone()),
|
||||
msi::Value::Str(directory.parent_key.clone()),
|
||||
msi::Value::Str(directory.name.clone()),
|
||||
]);
|
||||
}
|
||||
package.insert_rows(
|
||||
msi::Insert::into("Directory")
|
||||
.row(vec![
|
||||
msi::Value::from("TARGETDIR"),
|
||||
msi::Value::Null,
|
||||
msi::Value::from("SourceDir"),
|
||||
])
|
||||
.row(vec![
|
||||
msi::Value::from("ProgramFilesFolder"),
|
||||
msi::Value::from("TARGETDIR"),
|
||||
msi::Value::from("."),
|
||||
])
|
||||
.rows(rows),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Creates and populates the `Feature` database table for the package. The
|
||||
// package will have a single main feature that installs everything.
|
||||
fn create_feature_table(package: &mut Package, settings: &Settings) -> crate::Result<()> {
|
||||
package.create_table(
|
||||
"Feature",
|
||||
vec![
|
||||
msi::Column::build("Feature").primary_key().id_string(38),
|
||||
msi::Column::build("Feature_Parent")
|
||||
.nullable()
|
||||
.foreign_key("Feature", 1)
|
||||
.id_string(38),
|
||||
msi::Column::build("Title").nullable().text_string(64),
|
||||
msi::Column::build("Description")
|
||||
.nullable()
|
||||
.text_string(255),
|
||||
msi::Column::build("Display")
|
||||
.nullable()
|
||||
.range(0, 0x7fff)
|
||||
.int16(),
|
||||
msi::Column::build("Level").range(0, 0x7fff).int16(),
|
||||
msi::Column::build("Directory_")
|
||||
.nullable()
|
||||
.foreign_key("Directory", 1)
|
||||
.id_string(72),
|
||||
msi::Column::build("Attributes").int16(),
|
||||
],
|
||||
)?;
|
||||
package.insert_rows(msi::Insert::into("Feature").row(vec![
|
||||
msi::Value::from(MAIN_FEATURE_NAME),
|
||||
msi::Value::Null,
|
||||
msi::Value::from(settings.bundle_name()),
|
||||
msi::Value::Null,
|
||||
msi::Value::Int(1),
|
||||
msi::Value::Int(3),
|
||||
msi::Value::from("INSTALLDIR"),
|
||||
msi::Value::Int(0),
|
||||
]))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Creates and populates the `Component` database table for the package. One
|
||||
// component is created for each subdirectory under in the install dir.
|
||||
fn create_component_table(
|
||||
package: &mut Package,
|
||||
package_guid: Uuid,
|
||||
directories: &[DirectoryInfo],
|
||||
) -> crate::Result<()> {
|
||||
package.create_table(
|
||||
"Component",
|
||||
vec![
|
||||
msi::Column::build("Component").primary_key().id_string(72),
|
||||
msi::Column::build("ComponentId")
|
||||
.nullable()
|
||||
.category(msi::Category::Guid)
|
||||
.string(38),
|
||||
msi::Column::build("Directory_")
|
||||
.nullable()
|
||||
.foreign_key("Directory", 1)
|
||||
.id_string(72),
|
||||
msi::Column::build("Attributes").int16(),
|
||||
msi::Column::build("Condition")
|
||||
.nullable()
|
||||
.category(msi::Category::Condition)
|
||||
.string(255),
|
||||
msi::Column::build("KeyPath").nullable().id_string(72),
|
||||
],
|
||||
)?;
|
||||
let mut rows = Vec::new();
|
||||
for directory in directories.iter() {
|
||||
if !directory.files.is_empty() {
|
||||
let hash_input = directory.files.join("/");
|
||||
rows.push(vec![
|
||||
msi::Value::Str(directory.key.clone()),
|
||||
msi::Value::from(Uuid::new_v5(&package_guid, &hash_input)),
|
||||
msi::Value::Str(directory.key.clone()),
|
||||
msi::Value::Int(0),
|
||||
msi::Value::Null,
|
||||
msi::Value::Str(directory.files[0].clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
package.insert_rows(msi::Insert::into("Component").rows(rows))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Creates and populates the `FeatureComponents` database table for the
|
||||
// package. All components are added to the package's single main feature.
|
||||
fn create_feature_components_table(
|
||||
package: &mut Package,
|
||||
directories: &[DirectoryInfo],
|
||||
) -> crate::Result<()> {
|
||||
package.create_table(
|
||||
"FeatureComponents",
|
||||
vec![
|
||||
msi::Column::build("Feature_")
|
||||
.primary_key()
|
||||
.foreign_key("Component", 1)
|
||||
.id_string(38),
|
||||
msi::Column::build("Component_")
|
||||
.primary_key()
|
||||
.foreign_key("Component", 1)
|
||||
.id_string(72),
|
||||
],
|
||||
)?;
|
||||
let mut rows = Vec::new();
|
||||
for directory in directories.iter() {
|
||||
if !directory.files.is_empty() {
|
||||
rows.push(vec![
|
||||
msi::Value::from(MAIN_FEATURE_NAME),
|
||||
msi::Value::Str(directory.key.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
package.insert_rows(msi::Insert::into("FeatureComponents").rows(rows))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Creates and populates the `Media` database table for the package, with one
|
||||
// entry for each CAB archive within the package.
|
||||
fn create_media_table(package: &mut Package, cabinets: &[CabinetInfo]) -> crate::Result<()> {
|
||||
package.create_table(
|
||||
"Media",
|
||||
vec![
|
||||
msi::Column::build("DiskId")
|
||||
.primary_key()
|
||||
.range(1, 0x7fff)
|
||||
.int16(),
|
||||
msi::Column::build("LastSequence").range(0, 0x7fff).int16(),
|
||||
msi::Column::build("DiskPrompt").nullable().text_string(64),
|
||||
msi::Column::build("Cabinet")
|
||||
.nullable()
|
||||
.category(msi::Category::Cabinet)
|
||||
.string(255),
|
||||
msi::Column::build("VolumeLabel").nullable().text_string(32),
|
||||
msi::Column::build("Source")
|
||||
.nullable()
|
||||
.category(msi::Category::Property)
|
||||
.string(32),
|
||||
],
|
||||
)?;
|
||||
let mut disk_id: i32 = 0;
|
||||
let mut last_seq: i32 = 0;
|
||||
let mut rows = Vec::new();
|
||||
for cabinet in cabinets.iter() {
|
||||
disk_id += 1;
|
||||
last_seq += cabinet.resources.len() as i32;
|
||||
rows.push(vec![
|
||||
msi::Value::Int(disk_id),
|
||||
msi::Value::Int(last_seq),
|
||||
msi::Value::Null,
|
||||
msi::Value::Str(format!("#{}", cabinet.name)),
|
||||
msi::Value::Null,
|
||||
msi::Value::Null,
|
||||
]);
|
||||
}
|
||||
package.insert_rows(msi::Insert::into("Media").rows(rows))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Creates and populates the `File` database table for the package, with one
|
||||
// entry for each resource file to be installed (including the main
|
||||
// executable).
|
||||
fn create_file_table(package: &mut Package, cabinets: &[CabinetInfo]) -> crate::Result<()> {
|
||||
package.create_table(
|
||||
"File",
|
||||
vec![
|
||||
msi::Column::build("File").primary_key().id_string(72),
|
||||
msi::Column::build("Component_")
|
||||
.foreign_key("Component", 1)
|
||||
.id_string(72),
|
||||
msi::Column::build("FileName")
|
||||
.category(msi::Category::Filename)
|
||||
.string(255),
|
||||
msi::Column::build("FileSize").range(0, 0x7fffffff).int32(),
|
||||
msi::Column::build("Version")
|
||||
.nullable()
|
||||
.category(msi::Category::Version)
|
||||
.string(72),
|
||||
msi::Column::build("Language")
|
||||
.nullable()
|
||||
.category(msi::Category::Language)
|
||||
.string(20),
|
||||
msi::Column::build("Attributes")
|
||||
.nullable()
|
||||
.range(0, 0x7fff)
|
||||
.int16(),
|
||||
msi::Column::build("Sequence").range(1, 0x7fff).int16(),
|
||||
],
|
||||
)?;
|
||||
let mut rows = Vec::new();
|
||||
let mut sequence: i32 = 1;
|
||||
for cabinet in cabinets.iter() {
|
||||
for resource in cabinet.resources.iter() {
|
||||
rows.push(vec![
|
||||
msi::Value::Str(format!("r{:04}", sequence)),
|
||||
msi::Value::Str(resource.component_key.clone()),
|
||||
msi::Value::Str(resource.filename.clone()),
|
||||
msi::Value::Int(resource.size as i32),
|
||||
msi::Value::Null,
|
||||
msi::Value::Null,
|
||||
msi::Value::from(FILE_ATTR_VITAL),
|
||||
msi::Value::Int(sequence),
|
||||
]);
|
||||
sequence += 1;
|
||||
}
|
||||
}
|
||||
package.insert_rows(msi::Insert::into("File").rows(rows))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_app_icon<W: Write>(writer: &mut W, settings: &Settings) -> crate::Result<()> {
|
||||
// Prefer ICO files.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() == Some(OsStr::new("ico")) {
|
||||
io::copy(&mut fs::File::open(icon_path)?, writer)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// TODO: Convert from other formats.
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
// An OSX package is laid out like:
|
||||
//
|
||||
// foobar.app # Actually a directory
|
||||
// Contents # A further subdirectory
|
||||
// Info.plist # An xml file containing the app's metadata
|
||||
// MacOS # A directory to hold executable binary files
|
||||
// foobar # The main binary executable of the app
|
||||
// foobar_helper # A helper application, possibly provitidng a CLI
|
||||
// Resources # Data files such as images, sounds, translations and nib files
|
||||
// en.lproj # Folder containing english translation strings/data
|
||||
// Frameworks # A directory containing private frameworks (shared libraries)
|
||||
// ... # Any other optional files the developer wants to place here
|
||||
//
|
||||
// See https://developer.apple.com/go/?id=bundle-structure for a full
|
||||
// explanation.
|
||||
//
|
||||
// Currently, cargo-bundle does not support Frameworks, nor does it support placing arbitrary
|
||||
// files into the `Contents` directory of the bundle.
|
||||
|
||||
use super::common;
|
||||
use chrono;
|
||||
use dirs;
|
||||
use icns;
|
||||
use image::{self, GenericImage};
|
||||
use std::cmp::min;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
use std::io::prelude::*;
|
||||
use std::io::{self, BufWriter};
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::{ResultExt, Settings};
|
||||
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let app_bundle_name = format!("{}.app", settings.bundle_name());
|
||||
common::print_bundling(&app_bundle_name)?;
|
||||
let app_bundle_path = settings
|
||||
.project_out_directory()
|
||||
.join("bundle/osx")
|
||||
.join(&app_bundle_name);
|
||||
if app_bundle_path.exists() {
|
||||
fs::remove_dir_all(&app_bundle_path)
|
||||
.chain_err(|| format!("Failed to remove old {}", app_bundle_name))?;
|
||||
}
|
||||
let bundle_directory = app_bundle_path.join("Contents");
|
||||
fs::create_dir_all(&bundle_directory).chain_err(|| {
|
||||
format!(
|
||||
"Failed to create bundle directory at {:?}",
|
||||
bundle_directory
|
||||
)
|
||||
})?;
|
||||
|
||||
let resources_dir = bundle_directory.join("Resources");
|
||||
|
||||
let bundle_icon_file: Option<PathBuf> =
|
||||
{ create_icns_file(&resources_dir, settings).chain_err(|| "Failed to create app icon")? };
|
||||
|
||||
create_info_plist(&bundle_directory, bundle_icon_file, settings)
|
||||
.chain_err(|| "Failed to create Info.plist")?;
|
||||
|
||||
copy_frameworks_to_bundle(&bundle_directory, settings)
|
||||
.chain_err(|| "Failed to bundle frameworks")?;
|
||||
|
||||
for src in settings.resource_files() {
|
||||
let src = src?;
|
||||
let dest = resources_dir.join(common::resource_relpath(&src));
|
||||
common::copy_file(&src, &dest)
|
||||
.chain_err(|| format!("Failed to copy resource file {:?}", src))?;
|
||||
}
|
||||
|
||||
copy_binary_to_bundle(&bundle_directory, settings)
|
||||
.chain_err(|| format!("Failed to copy binary from {:?}", settings.binary_path()))?;
|
||||
|
||||
Ok(vec![app_bundle_path])
|
||||
}
|
||||
|
||||
fn copy_binary_to_bundle(bundle_directory: &Path, settings: &Settings) -> crate::Result<()> {
|
||||
let dest_dir = bundle_directory.join("MacOS");
|
||||
common::copy_file(
|
||||
settings.binary_path(),
|
||||
&dest_dir.join(settings.binary_name()),
|
||||
)
|
||||
}
|
||||
|
||||
fn create_info_plist(
|
||||
bundle_dir: &Path,
|
||||
bundle_icon_file: Option<PathBuf>,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<()> {
|
||||
let build_number = chrono::Utc::now().format("%Y%m%d.%H%M%S");
|
||||
let file = &mut common::create_file(&bundle_dir.join("Info.plist"))?;
|
||||
write!(
|
||||
file,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \
|
||||
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
|
||||
<plist version=\"1.0\">\n\
|
||||
<dict>\n"
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleDevelopmentRegion</key>\n \
|
||||
<string>English</string>\n"
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleDisplayName</key>\n <string>{}</string>\n",
|
||||
settings.bundle_name()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleExecutable</key>\n <string>{}</string>\n",
|
||||
settings.binary_name()
|
||||
)?;
|
||||
if let Some(path) = bundle_icon_file {
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleIconFile</key>\n <string>{}</string>\n",
|
||||
path.file_name().unwrap().to_string_lossy()
|
||||
)?;
|
||||
}
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleIdentifier</key>\n <string>{}</string>\n",
|
||||
settings.bundle_identifier()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleInfoDictionaryVersion</key>\n \
|
||||
<string>6.0</string>\n"
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleName</key>\n <string>{}</string>\n",
|
||||
settings.bundle_name()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundlePackageType</key>\n <string>APPL</string>\n"
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleShortVersionString</key>\n <string>{}</string>\n",
|
||||
settings.version_string()
|
||||
)?;
|
||||
write!(
|
||||
file,
|
||||
" <key>CFBundleVersion</key>\n <string>{}</string>\n",
|
||||
build_number
|
||||
)?;
|
||||
write!(file, " <key>CSResourcesFileMapped</key>\n <true/>\n")?;
|
||||
if let Some(category) = settings.app_category() {
|
||||
write!(
|
||||
file,
|
||||
" <key>LSApplicationCategoryType</key>\n \
|
||||
<string>{}</string>\n",
|
||||
category.osx_application_category_type()
|
||||
)?;
|
||||
}
|
||||
if let Some(version) = settings.osx_minimum_system_version() {
|
||||
write!(
|
||||
file,
|
||||
" <key>LSMinimumSystemVersion</key>\n \
|
||||
<string>{}</string>\n",
|
||||
version
|
||||
)?;
|
||||
}
|
||||
write!(file, " <key>LSRequiresCarbon</key>\n <true/>\n")?;
|
||||
write!(file, " <key>NSHighResolutionCapable</key>\n <true/>\n")?;
|
||||
if let Some(copyright) = settings.copyright_string() {
|
||||
write!(
|
||||
file,
|
||||
" <key>NSHumanReadableCopyright</key>\n \
|
||||
<string>{}</string>\n",
|
||||
copyright
|
||||
)?;
|
||||
}
|
||||
write!(file, "</dict>\n</plist>\n")?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_framework_from(dest_dir: &Path, framework: &str, src_dir: &Path) -> crate::Result<bool> {
|
||||
let src_name = format!("{}.framework", framework);
|
||||
let src_path = src_dir.join(&src_name);
|
||||
if src_path.exists() {
|
||||
common::copy_dir(&src_path, &dest_dir.join(&src_name))?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_frameworks_to_bundle(bundle_directory: &Path, settings: &Settings) -> crate::Result<()> {
|
||||
let frameworks = settings.osx_frameworks();
|
||||
if frameworks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let dest_dir = bundle_directory.join("Frameworks");
|
||||
fs::create_dir_all(&bundle_directory)
|
||||
.chain_err(|| format!("Failed to create Frameworks directory at {:?}", dest_dir))?;
|
||||
for framework in frameworks.iter() {
|
||||
if framework.ends_with(".framework") {
|
||||
let src_path = PathBuf::from(framework);
|
||||
let src_name = src_path.file_name().unwrap();
|
||||
common::copy_dir(&src_path, &dest_dir.join(&src_name))?;
|
||||
continue;
|
||||
} else if framework.contains("/") {
|
||||
bail!(
|
||||
"Framework path should have .framework extension: {}",
|
||||
framework
|
||||
);
|
||||
}
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
if copy_framework_from(&dest_dir, framework, &home_dir.join("Library/Frameworks/"))? {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if copy_framework_from(&dest_dir, framework, &PathBuf::from("/Library/Frameworks/"))?
|
||||
|| copy_framework_from(
|
||||
&dest_dir,
|
||||
framework,
|
||||
&PathBuf::from("/Network/Library/Frameworks/"),
|
||||
)?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bail!("Could not locate {}.framework", framework);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Given a list of icon files, try to produce an ICNS file in the resources
|
||||
/// directory and return the path to it. Returns `Ok(None)` if no usable icons
|
||||
/// were provided.
|
||||
fn create_icns_file(resources_dir: &PathBuf, settings: &Settings) -> crate::Result<Option<PathBuf>> {
|
||||
if settings.icon_files().count() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If one of the icon files is already an ICNS file, just use that.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() == Some(OsStr::new("icns")) {
|
||||
let mut dest_path = resources_dir.to_path_buf();
|
||||
dest_path.push(icon_path.file_name().unwrap());
|
||||
common::copy_file(&icon_path, &dest_path)?;
|
||||
return Ok(Some(dest_path));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, read available images and pack them into a new ICNS file.
|
||||
let mut family = icns::IconFamily::new();
|
||||
|
||||
fn add_icon_to_family(
|
||||
icon: image::DynamicImage,
|
||||
density: u32,
|
||||
family: &mut icns::IconFamily,
|
||||
) -> io::Result<()> {
|
||||
// Try to add this image to the icon family. Ignore images whose sizes
|
||||
// don't map to any ICNS icon type; print warnings and skip images that
|
||||
// fail to encode.
|
||||
match icns::IconType::from_pixel_size_and_density(icon.width(), icon.height(), density) {
|
||||
Some(icon_type) => {
|
||||
if !family.has_icon_with_type(icon_type) {
|
||||
let icon = r#try!(make_icns_image(icon));
|
||||
r#try!(family.add_icon_with_type(&icon, icon_type));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"No matching IconType",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
let mut images_to_resize: Vec<(image::DynamicImage, u32, u32)> = vec![];
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
let icon = r#try!(image::open(&icon_path));
|
||||
let density = if common::is_retina(&icon_path) { 2 } else { 1 };
|
||||
let (w, h) = icon.dimensions();
|
||||
let orig_size = min(w, h);
|
||||
let next_size_down = 2f32.powf((orig_size as f32).log2().floor()) as u32;
|
||||
if orig_size > next_size_down {
|
||||
images_to_resize.push((icon, next_size_down, density));
|
||||
} else {
|
||||
r#try!(add_icon_to_family(icon, density, &mut family));
|
||||
}
|
||||
}
|
||||
|
||||
for (icon, next_size_down, density) in images_to_resize {
|
||||
let icon = icon.resize_exact(next_size_down, next_size_down, image::Lanczos3);
|
||||
r#try!(add_icon_to_family(icon, density, &mut family));
|
||||
}
|
||||
|
||||
if !family.is_empty() {
|
||||
r#try!(fs::create_dir_all(resources_dir));
|
||||
let mut dest_path = resources_dir.clone();
|
||||
dest_path.push(settings.bundle_name());
|
||||
dest_path.set_extension("icns");
|
||||
let icns_file = BufWriter::new(r#try!(File::create(&dest_path)));
|
||||
r#try!(family.write(icns_file));
|
||||
return Ok(Some(dest_path));
|
||||
}
|
||||
|
||||
bail!("No usable icon files found.");
|
||||
}
|
||||
|
||||
/// Converts an image::DynamicImage into an icns::Image.
|
||||
fn make_icns_image(img: image::DynamicImage) -> io::Result<icns::Image> {
|
||||
let pixel_format = match img.color() {
|
||||
image::ColorType::RGBA(8) => icns::PixelFormat::RGBA,
|
||||
image::ColorType::RGB(8) => icns::PixelFormat::RGB,
|
||||
image::ColorType::GrayA(8) => icns::PixelFormat::GrayAlpha,
|
||||
image::ColorType::Gray(8) => icns::PixelFormat::Gray,
|
||||
_ => {
|
||||
let msg = format!("unsupported ColorType: {:?}", img.color());
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
|
||||
}
|
||||
};
|
||||
icns::Image::from_data(pixel_format, img.width(), img.height(), img.raw_pixels())
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
use std::path::PathBuf;
|
||||
use crate::Settings;
|
||||
|
||||
pub fn bundle_project(_settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
unimplemented!();
|
||||
}
|
|
@ -0,0 +1,637 @@
|
|||
use super::category::AppCategory;
|
||||
use clap::ArgMatches;
|
||||
use glob;
|
||||
use std;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use target_build_utils::TargetInfo;
|
||||
use toml;
|
||||
use walkdir;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PackageType {
|
||||
OsxBundle,
|
||||
IosBundle,
|
||||
WindowsMsi,
|
||||
Deb,
|
||||
Rpm,
|
||||
}
|
||||
|
||||
impl PackageType {
|
||||
pub fn from_short_name(name: &str) -> Option<PackageType> {
|
||||
// Other types we may eventually want to support: apk
|
||||
match name {
|
||||
"deb" => Some(PackageType::Deb),
|
||||
"ios" => Some(PackageType::IosBundle),
|
||||
"msi" => Some(PackageType::WindowsMsi),
|
||||
"osx" => Some(PackageType::OsxBundle),
|
||||
"rpm" => Some(PackageType::Rpm),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match *self {
|
||||
PackageType::Deb => "deb",
|
||||
PackageType::IosBundle => "ios",
|
||||
PackageType::WindowsMsi => "msi",
|
||||
PackageType::OsxBundle => "osx",
|
||||
PackageType::Rpm => "rpm",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all() -> &'static [PackageType] {
|
||||
ALL_PACKAGE_TYPES
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_PACKAGE_TYPES: &[PackageType] = &[
|
||||
PackageType::Deb,
|
||||
PackageType::IosBundle,
|
||||
PackageType::WindowsMsi,
|
||||
PackageType::OsxBundle,
|
||||
PackageType::Rpm,
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BuildArtifact {
|
||||
Main,
|
||||
Bin(String),
|
||||
Example(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct BundleSettings {
|
||||
// General settings:
|
||||
name: Option<String>,
|
||||
identifier: Option<String>,
|
||||
icon: Option<Vec<String>>,
|
||||
version: Option<String>,
|
||||
resources: Option<Vec<String>>,
|
||||
copyright: Option<String>,
|
||||
category: Option<AppCategory>,
|
||||
short_description: Option<String>,
|
||||
long_description: Option<String>,
|
||||
script: Option<PathBuf>,
|
||||
// OS-specific settings:
|
||||
deb_depends: Option<Vec<String>>,
|
||||
osx_frameworks: Option<Vec<String>>,
|
||||
osx_minimum_system_version: Option<String>,
|
||||
// Bundles for other binaries/examples:
|
||||
bin: Option<HashMap<String, BundleSettings>>,
|
||||
example: Option<HashMap<String, BundleSettings>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct MetadataSettings {
|
||||
bundle: Option<BundleSettings>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct PackageSettings {
|
||||
name: String,
|
||||
version: String,
|
||||
description: String,
|
||||
homepage: Option<String>,
|
||||
authors: Option<Vec<String>>,
|
||||
metadata: Option<MetadataSettings>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct WorkspaceSettings {
|
||||
members: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct CargoSettings {
|
||||
package: Option<PackageSettings>, // "Ancestor" workspace Cargo.toml files may not have package info
|
||||
workspace: Option<WorkspaceSettings>, // "Ancestor" workspace Cargo.toml files may declare workspaces
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Settings {
|
||||
package: PackageSettings,
|
||||
package_type: Option<PackageType>, // If `None`, use the default package type for this os
|
||||
target: Option<(String, TargetInfo)>,
|
||||
features: Option<Vec<String>>,
|
||||
project_out_directory: PathBuf,
|
||||
build_artifact: BuildArtifact,
|
||||
is_release: bool,
|
||||
binary_path: PathBuf,
|
||||
binary_name: String,
|
||||
bundle_settings: BundleSettings,
|
||||
}
|
||||
|
||||
impl CargoSettings {
|
||||
/*
|
||||
Try to load a set of CargoSettings from a "Cargo.toml" file in the specified directory
|
||||
*/
|
||||
fn load(dir: &PathBuf) -> crate::Result<Self> {
|
||||
let toml_path = dir.join("Cargo.toml");
|
||||
let mut toml_str = String::new();
|
||||
let mut toml_file = File::open(toml_path)?;
|
||||
toml_file.read_to_string(&mut toml_str)?;
|
||||
toml::from_str(&toml_str).map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new(current_dir: PathBuf, matches: & ArgMatches) -> crate::Result<Self> {
|
||||
let package_type = match matches.value_of("format") {
|
||||
Some(name) => match PackageType::from_short_name(name) {
|
||||
Some(package_type) => Some(package_type),
|
||||
None => bail!("Unsupported bundle format: {}", name),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let build_artifact = if let Some(bin) = matches.value_of("bin") {
|
||||
BuildArtifact::Bin(bin.to_string())
|
||||
} else if let Some(example) = matches.value_of("example") {
|
||||
BuildArtifact::Example(example.to_string())
|
||||
} else {
|
||||
BuildArtifact::Main
|
||||
};
|
||||
let is_release = matches.is_present("release");
|
||||
let target = match matches.value_of("target") {
|
||||
Some(triple) => Some((triple.to_string(), TargetInfo::from_str(triple)?)),
|
||||
None => None,
|
||||
};
|
||||
let features = if matches.is_present("features") {
|
||||
Some(matches.values_of("features").unwrap().map(|s| s.to_string()).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cargo_settings = CargoSettings::load(¤t_dir)?;
|
||||
let package = match cargo_settings.package {
|
||||
Some(package_info) => package_info,
|
||||
None => bail!("No 'package' info found in 'Cargo.toml'"),
|
||||
};
|
||||
let workspace_dir = Settings::get_workspace_dir(¤t_dir);
|
||||
let target_dir = Settings::get_target_dir(&workspace_dir, &target, is_release, &build_artifact);
|
||||
let bundle_settings = if let Some(bundle_settings) = package
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.bundle.as_ref())
|
||||
{
|
||||
bundle_settings.clone()
|
||||
} else {
|
||||
bail!("No [package.metadata.bundle] section in Cargo.toml");
|
||||
};
|
||||
let (bundle_settings, binary_name) = match build_artifact {
|
||||
BuildArtifact::Main => (bundle_settings, package.name.clone()),
|
||||
BuildArtifact::Bin(ref name) => (
|
||||
bundle_settings_from_table(&bundle_settings.bin, "bin", name)?,
|
||||
name.clone(),
|
||||
),
|
||||
BuildArtifact::Example(ref name) => (
|
||||
bundle_settings_from_table(&bundle_settings.example, "example", name)?,
|
||||
name.clone(),
|
||||
),
|
||||
};
|
||||
let binary_path = target_dir.join(&binary_name);
|
||||
Ok(Settings {
|
||||
package,
|
||||
package_type,
|
||||
target,
|
||||
features,
|
||||
build_artifact,
|
||||
is_release,
|
||||
project_out_directory: target_dir,
|
||||
binary_path,
|
||||
binary_name,
|
||||
bundle_settings,
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
The target_dir where binaries will be compiled to by cargo can vary:
|
||||
- this directory is a member of a workspace project
|
||||
- overridden by CARGO_TARGET_DIR environment variable
|
||||
- specified in build.target-dir configuration key
|
||||
- if the build is a 'release' or 'debug' build
|
||||
|
||||
This function determines where 'target' dir is and suffixes it with 'release' or 'debug'
|
||||
to determine where the compiled binary will be located.
|
||||
*/
|
||||
fn get_target_dir(
|
||||
project_root_dir: &PathBuf,
|
||||
target: &Option<(String, TargetInfo)>,
|
||||
is_release: bool,
|
||||
build_artifact: &BuildArtifact,
|
||||
) -> PathBuf {
|
||||
let mut path = project_root_dir.join("target");
|
||||
if let &Some((ref triple, _)) = target {
|
||||
path.push(triple);
|
||||
}
|
||||
path.push(if is_release { "release" } else { "debug" });
|
||||
if let &BuildArtifact::Example(_) = build_artifact {
|
||||
path.push("examples");
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/*
|
||||
The specification of the Cargo.toml Manifest that covers the "workspace" section is here:
|
||||
https://doc.rust-lang.org/cargo/reference/manifest.html#the-workspace-section
|
||||
|
||||
Determining if the current project folder is part of a workspace:
|
||||
- Walk up the file system, looking for a Cargo.toml file.
|
||||
- Stop at the first one found.
|
||||
- If one is found before reaching "/" then this folder belongs to that parent workspace
|
||||
*/
|
||||
fn get_workspace_dir(current_dir: &PathBuf) -> PathBuf {
|
||||
let mut dir = current_dir.clone();
|
||||
while dir.pop() {
|
||||
let set = CargoSettings::load(&dir);
|
||||
if set.is_ok() {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found walking up the file system, return the starting directory
|
||||
current_dir.clone()
|
||||
}
|
||||
|
||||
/// Returns the directory where the bundle should be placed.
|
||||
pub fn project_out_directory(&self) -> &Path {
|
||||
&self.project_out_directory
|
||||
}
|
||||
|
||||
/// Returns the architecture for the binary being bundled (e.g. "arm" or
|
||||
/// "x86" or "x86_64").
|
||||
pub fn binary_arch(&self) -> &str {
|
||||
if let Some((_, ref info)) = self.target {
|
||||
info.target_arch()
|
||||
} else {
|
||||
std::env::consts::ARCH
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the file name of the binary being bundled.
|
||||
pub fn binary_name(&self) -> &str {
|
||||
&self.binary_name
|
||||
}
|
||||
|
||||
/// Returns the path to the binary being bundled.
|
||||
pub fn binary_path(&self) -> &Path {
|
||||
&self.binary_path
|
||||
}
|
||||
|
||||
/// If a specific package type was specified by the command-line, returns
|
||||
/// that package type; otherwise, if a target triple was specified by the
|
||||
/// command-line, returns the native package type(s) for that target;
|
||||
/// otherwise, returns the native package type(s) for the host platform.
|
||||
/// Fails if the host/target's native package type is not supported.
|
||||
pub fn package_types(&self) -> crate::Result<Vec<PackageType>> {
|
||||
if let Some(package_type) = self.package_type {
|
||||
Ok(vec![package_type])
|
||||
} else {
|
||||
let target_os = if let Some((_, ref info)) = self.target {
|
||||
info.target_os()
|
||||
} else {
|
||||
std::env::consts::OS
|
||||
};
|
||||
match target_os {
|
||||
"macos" => Ok(vec![PackageType::OsxBundle]),
|
||||
"ios" => Ok(vec![PackageType::IosBundle]),
|
||||
"linux" => Ok(vec![PackageType::Deb]), // TODO: Do Rpm too, once it's implemented.
|
||||
"windows" => Ok(vec![PackageType::WindowsMsi]),
|
||||
os => bail!("Native {} bundles not yet supported.", os),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the bundle is being cross-compiled, returns the target triple string
|
||||
/// (e.g. `"x86_64-apple-darwin"`). If the bundle is targeting the host
|
||||
/// environment, returns `None`.
|
||||
pub fn target_triple(&self) -> Option<&str> {
|
||||
match self.target {
|
||||
Some((ref triple, _)) => Some(triple.as_str()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the features that is being built.
|
||||
pub fn build_features(&self) -> Option<Vec<String>> {
|
||||
self.features.to_owned()
|
||||
}
|
||||
|
||||
/// Returns the artifact that is being bundled.
|
||||
pub fn build_artifact(&self) -> &BuildArtifact {
|
||||
&self.build_artifact
|
||||
}
|
||||
|
||||
/// Returns true if the bundle is being compiled in release mode, false if
|
||||
/// it's being compiled in debug mode.
|
||||
pub fn is_release_build(&self) -> bool {
|
||||
self.is_release
|
||||
}
|
||||
|
||||
pub fn bundle_name(&self) -> &str {
|
||||
self
|
||||
.bundle_settings
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap_or(&self.package.name)
|
||||
}
|
||||
|
||||
pub fn bundle_identifier(&self) -> &str {
|
||||
self
|
||||
.bundle_settings
|
||||
.identifier
|
||||
.as_ref()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
/// Returns an iterator over the icon files to be used for this bundle.
|
||||
pub fn icon_files(&self) -> ResourcePaths {
|
||||
match self.bundle_settings.icon {
|
||||
Some(ref paths) => ResourcePaths::new(paths.as_slice(), false),
|
||||
None => ResourcePaths::new(&[], false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the resource files to be included in this
|
||||
/// bundle.
|
||||
pub fn resource_files(&self) -> ResourcePaths {
|
||||
match self.bundle_settings.resources {
|
||||
Some(ref paths) => ResourcePaths::new(paths.as_slice(), true),
|
||||
None => ResourcePaths::new(&[], true),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version_string(&self) -> &str {
|
||||
self
|
||||
.bundle_settings
|
||||
.version
|
||||
.as_ref()
|
||||
.unwrap_or(&self.package.version)
|
||||
}
|
||||
|
||||
pub fn copyright_string(&self) -> Option<&str> {
|
||||
self.bundle_settings.copyright.as_ref().map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn author_names(&self) -> &[String] {
|
||||
match self.package.authors {
|
||||
Some(ref names) => names.as_slice(),
|
||||
None => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authors_comma_separated(&self) -> Option<String> {
|
||||
let names = self.author_names();
|
||||
if names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(names.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn homepage_url(&self) -> &str {
|
||||
&self
|
||||
.package
|
||||
.homepage
|
||||
.as_ref()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
pub fn app_category(&self) -> Option<AppCategory> {
|
||||
self.bundle_settings.category
|
||||
}
|
||||
|
||||
pub fn short_description(&self) -> &str {
|
||||
self
|
||||
.bundle_settings
|
||||
.short_description
|
||||
.as_ref()
|
||||
.unwrap_or(&self.package.description)
|
||||
}
|
||||
|
||||
pub fn long_description(&self) -> Option<&str> {
|
||||
self
|
||||
.bundle_settings
|
||||
.long_description
|
||||
.as_ref()
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn debian_dependencies(&self) -> &[String] {
|
||||
match self.bundle_settings.deb_depends {
|
||||
Some(ref dependencies) => dependencies.as_slice(),
|
||||
None => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn osx_frameworks(&self) -> &[String] {
|
||||
match self.bundle_settings.osx_frameworks {
|
||||
Some(ref frameworks) => frameworks.as_slice(),
|
||||
None => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn osx_minimum_system_version(&self) -> Option<&str> {
|
||||
self
|
||||
.bundle_settings
|
||||
.osx_minimum_system_version
|
||||
.as_ref()
|
||||
.map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
fn bundle_settings_from_table(
|
||||
opt_map: &Option<HashMap<String, BundleSettings>>,
|
||||
map_name: &str,
|
||||
bundle_name: &str,
|
||||
) -> crate::Result<BundleSettings> {
|
||||
if let Some(bundle_settings) = opt_map.as_ref().and_then(|map| map.get(bundle_name)) {
|
||||
Ok(bundle_settings.clone())
|
||||
} else {
|
||||
bail!(
|
||||
"No [package.metadata.bundle.{}.{}] section in Cargo.toml",
|
||||
map_name,
|
||||
bundle_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ResourcePaths<'a> {
|
||||
pattern_iter: std::slice::Iter<'a, String>,
|
||||
glob_iter: Option<glob::Paths>,
|
||||
walk_iter: Option<walkdir::IntoIter>,
|
||||
allow_walk: bool,
|
||||
}
|
||||
|
||||
impl<'a> ResourcePaths<'a> {
|
||||
fn new(patterns: &'a [String], allow_walk: bool) -> ResourcePaths<'a> {
|
||||
ResourcePaths {
|
||||
pattern_iter: patterns.iter(),
|
||||
glob_iter: None,
|
||||
walk_iter: None,
|
||||
allow_walk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ResourcePaths<'a> {
|
||||
type Item = crate::Result<PathBuf>;
|
||||
|
||||
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
|
||||
loop {
|
||||
if let Some(ref mut walk_entries) = self.walk_iter {
|
||||
if let Some(entry) = walk_entries.next() {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(error) => return Some(Err(crate::Error::from(error))),
|
||||
};
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
return Some(Ok(path.to_path_buf()));
|
||||
}
|
||||
}
|
||||
self.walk_iter = None;
|
||||
if let Some(ref mut glob_paths) = self.glob_iter {
|
||||
if let Some(glob_result) = glob_paths.next() {
|
||||
let path = match glob_result {
|
||||
Ok(path) => path,
|
||||
Err(error) => return Some(Err(crate::Error::from(error))),
|
||||
};
|
||||
if path.is_dir() {
|
||||
if self.allow_walk {
|
||||
let walk = walkdir::WalkDir::new(path);
|
||||
self.walk_iter = Some(walk.into_iter());
|
||||
continue;
|
||||
} else {
|
||||
let msg = format!("{:?} is a directory", path);
|
||||
return Some(Err(crate::Error::from(msg)));
|
||||
}
|
||||
}
|
||||
return Some(Ok(path));
|
||||
}
|
||||
}
|
||||
self.glob_iter = None;
|
||||
if let Some(pattern) = self.pattern_iter.next() {
|
||||
let glob = match glob::glob(pattern) {
|
||||
Ok(glob) => glob,
|
||||
Err(error) => return Some(Err(crate::Error::from(error))),
|
||||
};
|
||||
self.glob_iter = Some(glob);
|
||||
continue;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AppCategory, BundleSettings, CargoSettings};
|
||||
use toml;
|
||||
|
||||
#[test]
|
||||
fn parse_cargo_toml() {
|
||||
let toml_str = "\
|
||||
[package]\n\
|
||||
name = \"example\"\n\
|
||||
version = \"0.1.0\"\n\
|
||||
authors = [\"Jane Doe\"]\n\
|
||||
license = \"MIT\"\n\
|
||||
description = \"An example application.\"\n\
|
||||
build = \"build.rs\"\n\
|
||||
\n\
|
||||
[package.metadata.bundle]\n\
|
||||
name = \"Example Application\"\n\
|
||||
identifier = \"com.example.app\"\n\
|
||||
resources = [\"data\", \"foo/bar\"]\n\
|
||||
category = \"Puzzle Game\"\n\
|
||||
long_description = \"\"\"\n\
|
||||
This is an example of a\n\
|
||||
simple application.\n\
|
||||
\"\"\"\n\
|
||||
\n\
|
||||
[dependencies]\n\
|
||||
rand = \"0.4\"\n";
|
||||
let cargo_settings: CargoSettings = toml::from_str(toml_str).unwrap();
|
||||
let package = cargo_settings.package.unwrap();
|
||||
assert_eq!(package.name, "example");
|
||||
assert_eq!(package.version, "0.1.0");
|
||||
assert_eq!(package.description, "An example application.");
|
||||
assert_eq!(package.homepage, None);
|
||||
assert_eq!(package.authors, Some(vec!["Jane Doe".to_string()]));
|
||||
assert!(package.metadata.is_some());
|
||||
let metadata = package.metadata.as_ref().unwrap();
|
||||
assert!(metadata.bundle.is_some());
|
||||
let bundle = metadata.bundle.as_ref().unwrap();
|
||||
assert_eq!(bundle.name, Some("Example Application".to_string()));
|
||||
assert_eq!(bundle.identifier, Some("com.example.app".to_string()));
|
||||
assert_eq!(bundle.icon, None);
|
||||
assert_eq!(bundle.version, None);
|
||||
assert_eq!(
|
||||
bundle.resources,
|
||||
Some(vec!["data".to_string(), "foo/bar".to_string()])
|
||||
);
|
||||
assert_eq!(bundle.category, Some(AppCategory::PuzzleGame));
|
||||
assert_eq!(
|
||||
bundle.long_description,
|
||||
Some(
|
||||
"This is an example of a\n\
|
||||
simple application.\n"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bin_and_example_bundles() {
|
||||
let toml_str = "\
|
||||
[package]\n\
|
||||
name = \"example\"\n\
|
||||
version = \"0.1.0\"\n\
|
||||
description = \"An example application.\"\n\
|
||||
\n\
|
||||
[package.metadata.bundle.bin.foo]\n\
|
||||
name = \"Foo App\"\n\
|
||||
\n\
|
||||
[package.metadata.bundle.bin.bar]\n\
|
||||
name = \"Bar App\"\n\
|
||||
\n\
|
||||
[package.metadata.bundle.example.baz]\n\
|
||||
name = \"Baz Example\"\n\
|
||||
\n\
|
||||
[[bin]]\n\
|
||||
name = \"foo\"\n
|
||||
\n\
|
||||
[[bin]]\n\
|
||||
name = \"bar\"\n
|
||||
\n\
|
||||
[[example]]\n\
|
||||
name = \"baz\"\n";
|
||||
let cargo_settings: CargoSettings = toml::from_str(toml_str).unwrap();
|
||||
assert!(cargo_settings.package.is_some());
|
||||
let package = cargo_settings.package.as_ref().unwrap();
|
||||
assert!(package.metadata.is_some());
|
||||
let metadata = package.metadata.as_ref().unwrap();
|
||||
assert!(metadata.bundle.is_some());
|
||||
let bundle = metadata.bundle.as_ref().unwrap();
|
||||
assert!(bundle.example.is_some());
|
||||
|
||||
let bins = bundle.bin.as_ref().unwrap();
|
||||
assert!(bins.contains_key("foo"));
|
||||
let foo: &BundleSettings = bins.get("foo").unwrap();
|
||||
assert_eq!(foo.name, Some("Foo App".to_string()));
|
||||
assert!(bins.contains_key("bar"));
|
||||
let bar: &BundleSettings = bins.get("bar").unwrap();
|
||||
assert_eq!(bar.name, Some("Bar App".to_string()));
|
||||
|
||||
let examples = bundle.example.as_ref().unwrap();
|
||||
assert!(examples.contains_key("baz"));
|
||||
let baz: &BundleSettings = examples.get("baz").unwrap();
|
||||
assert_eq!(baz.name, Some("Baz Example".to_string()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
extern crate ar;
|
||||
extern crate cab;
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate dirs;
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
extern crate glob;
|
||||
extern crate icns;
|
||||
extern crate image;
|
||||
extern crate libflate;
|
||||
extern crate md5;
|
||||
extern crate msi;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate strsim;
|
||||
extern crate tar;
|
||||
extern crate target_build_utils;
|
||||
extern crate term;
|
||||
extern crate toml;
|
||||
extern crate uuid;
|
||||
extern crate walkdir;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate tempfile;
|
||||
|
||||
mod bundle;
|
||||
|
||||
use crate::bundle::{bundle_project, BuildArtifact, PackageType, Settings};
|
||||
use clap::{App, AppSettings, Arg, SubCommand};
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Glob(::glob::GlobError);
|
||||
GlobPattern(::glob::PatternError);
|
||||
Io(::std::io::Error);
|
||||
Image(::image::ImageError);
|
||||
Target(::target_build_utils::Error);
|
||||
Term(::term::Error);
|
||||
Toml(::toml::de::Error);
|
||||
Walkdir(::walkdir::Error);
|
||||
}
|
||||
errors { }
|
||||
}
|
||||
|
||||
/// Runs `cargo build` to make sure the binary file is up-to-date.
|
||||
fn build_project_if_unbuilt(settings: &Settings) -> crate::Result<()> {
|
||||
let mut args = vec!["build".to_string()];
|
||||
if let Some(triple) = settings.target_triple() {
|
||||
args.push(format!("--target={}", triple));
|
||||
}
|
||||
match settings.build_artifact() {
|
||||
&BuildArtifact::Main => {}
|
||||
&BuildArtifact::Bin(ref name) => {
|
||||
args.push(format!("--bin={}", name));
|
||||
}
|
||||
&BuildArtifact::Example(ref name) => {
|
||||
args.push(format!("--example={}", name));
|
||||
}
|
||||
}
|
||||
if settings.is_release_build() {
|
||||
args.push("--release".to_string());
|
||||
}
|
||||
|
||||
match settings.build_features() {
|
||||
Some(features) => {
|
||||
args.push(format!("--features={}", features.join(" ")));
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
|
||||
let status = process::Command::new("cargo").args(args).status()?;
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"Result of `cargo build` operation was unsuccessful: {}",
|
||||
status
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run() -> crate::Result<()> {
|
||||
let all_formats: Vec<&str> = PackageType::all()
|
||||
.iter()
|
||||
.map(PackageType::short_name)
|
||||
.collect();
|
||||
let m = App::new("cargo-proton-bundle")
|
||||
.version(format!("v{}", crate_version!()).as_str())
|
||||
.bin_name("cargo")
|
||||
.setting(AppSettings::GlobalVersion)
|
||||
.setting(AppSettings::SubcommandRequired)
|
||||
.subcommand(
|
||||
SubCommand::with_name("proton-bundle")
|
||||
.author("George Burton <burtonageo@gmail.com>, Lucas Fernandes Gonçalves Nogueira <lucas@quasar.dev>, Daniel Thompson-Yvetot <denjell@quasar.dev>")
|
||||
.about("Bundle Rust executables into OS bundles")
|
||||
.setting(AppSettings::DisableVersion)
|
||||
.setting(AppSettings::UnifiedHelpMessage)
|
||||
.arg(
|
||||
Arg::with_name("bin")
|
||||
.long("bin")
|
||||
.value_name("NAME")
|
||||
.help("Bundle the specified binary"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("example")
|
||||
.long("example")
|
||||
.value_name("NAME")
|
||||
.conflicts_with("bin")
|
||||
.help("Bundle the specified example"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("format")
|
||||
.long("format")
|
||||
.value_name("FORMAT")
|
||||
.possible_values(&all_formats)
|
||||
.help("Which bundle format to produce"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("release")
|
||||
.long("release")
|
||||
.help("Build a bundle from a target built in release mode"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("target")
|
||||
.long("target")
|
||||
.value_name("TRIPLE")
|
||||
.help("Build a bundle for the target triple"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("features")
|
||||
.long("features")
|
||||
.value_name("FEATURES")
|
||||
.multiple(true)
|
||||
.help("Which features to build"),
|
||||
),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if let Some(m) = m.subcommand_matches("proton-bundle") {
|
||||
let output_paths = env::current_dir()
|
||||
.map_err(From::from)
|
||||
.and_then(|d| Settings::new(d, m))
|
||||
.and_then(|s| {
|
||||
r#try!(build_project_if_unbuilt(&s));
|
||||
Ok(s)
|
||||
})
|
||||
.and_then(bundle_project)?;
|
||||
bundle::print_finished(&output_paths)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = run() {
|
||||
bundle::print_error(&error).unwrap();
|
||||
}
|
||||
}
|
48
ui/proton.h
48
ui/proton.h
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2017 Serge Zaitsev
|
||||
* Copyright (c) 2017 Serge Zaitsev, (c) 2019 Quasar Framework
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -1575,7 +1575,7 @@ WEBVIEW_API void webview_dialog(struct webview *w,
|
|||
IShellItem *res = NULL;
|
||||
WCHAR *ws = NULL;
|
||||
char *s = NULL;
|
||||
FILEOPENDIALOGOPTIONS opts, add_opts;
|
||||
FILEOPENDIALOGOPTIONS opts = 0, add_opts = 0;
|
||||
if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN) {
|
||||
if (CoCreateInstance(
|
||||
iid_unref(&CLSID_FileOpenDialog), NULL, CLSCTX_INPROC_SERVER,
|
||||
|
@ -2062,6 +2062,49 @@ WEBVIEW_API int webview_init(struct webview *w) {
|
|||
item = create_menu_item(title, "terminate:", "q");
|
||||
objc_msgSend(appMenu, sel_registerName("addItem:"), item);
|
||||
|
||||
id editMenuItem =
|
||||
objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("alloc"));
|
||||
objc_msgSend(editMenuItem,
|
||||
sel_registerName("initWithTitle:action:keyEquivalent:"), get_nsstring("Edit"),
|
||||
NULL, get_nsstring(""));
|
||||
|
||||
/***
|
||||
Edit menu
|
||||
***/
|
||||
|
||||
id editMenu =
|
||||
objc_msgSend((id)objc_getClass("NSMenu"), sel_registerName("alloc"));
|
||||
objc_msgSend(editMenu, sel_registerName("initWithTitle:"), get_nsstring("Edit"));
|
||||
objc_msgSend(editMenu, sel_registerName("autorelease"));
|
||||
|
||||
objc_msgSend(editMenuItem, sel_registerName("setSubmenu:"), editMenu);
|
||||
objc_msgSend(menubar, sel_registerName("addItem:"), editMenuItem);
|
||||
|
||||
item = create_menu_item(get_nsstring("Undo"), "undo:", "z");
|
||||
objc_msgSend(editMenu, sel_registerName("addItem:"), item);
|
||||
|
||||
item = create_menu_item(get_nsstring("Redo"), "redo:", "y");
|
||||
objc_msgSend(editMenu, sel_registerName("addItem:"), item);
|
||||
|
||||
item = objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("separatorItem"));
|
||||
objc_msgSend(editMenu, sel_registerName("addItem:"), item);
|
||||
|
||||
item = create_menu_item(get_nsstring("Cut"), "cut:", "x");
|
||||
objc_msgSend(editMenu, sel_registerName("addItem:"), item);
|
||||
|
||||
item = create_menu_item(get_nsstring("Copy"), "copy:", "c");
|
||||
objc_msgSend(editMenu, sel_registerName("addItem:"), item);
|
||||
|
||||
item = create_menu_item(get_nsstring("Paste"), "paste:", "v");
|
||||
objc_msgSend(editMenu, sel_registerName("addItem:"), item);
|
||||
|
||||
item = create_menu_item(get_nsstring("Select All"), "selectAll:", "a");
|
||||
objc_msgSend(editMenu, sel_registerName("addItem:"), item);
|
||||
|
||||
/***
|
||||
Finalize menubar
|
||||
***/
|
||||
|
||||
objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"),
|
||||
sel_registerName("sharedApplication")),
|
||||
sel_registerName("setMainMenu:"), menubar);
|
||||
|
@ -2145,6 +2188,7 @@ WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g,
|
|||
objc_msgSend(w->priv.window, sel_registerName("setOpaque:"), 0);
|
||||
objc_msgSend(w->priv.window,
|
||||
sel_registerName("setTitlebarAppearsTransparent:"), 1);
|
||||
objc_msgSend(w->priv.webview, sel_registerName("_setDrawsBackground:"), 0);
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_dialog(struct webview *w,
|
||||
|
|
Loading…
Reference in New Issue