Skip to content

Commit cf8fda2

Browse files
committed
+windows
1 parent 3a2c940 commit cf8fda2

File tree

11 files changed

+209
-62
lines changed

11 files changed

+209
-62
lines changed

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
source ~/.cargo/env

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
clippy:
3636
strategy:
3737
matrix:
38-
os: [ubuntu-latest, macos-latest]
38+
os: [ubuntu-latest, macos-latest, windows-latest]
3939
runs-on: ${{ matrix.os }}
4040
steps:
4141
- uses: actions/checkout@v4
@@ -63,7 +63,7 @@ jobs:
6363
needs: fmt
6464
strategy:
6565
matrix:
66-
os: [ubuntu-latest, macos-latest]
66+
os: [ubuntu-latest, macos-latest, windows-latest]
6767
runs-on: ${{ matrix.os }}
6868
steps:
6969
- uses: actions/checkout@v4

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Python 2.7.18
5555
- `x86_64` & `arm64`
5656

5757
> [!TIP]
58+
>
5859
> We have gone to good lengths to make `pkgx` (and the packages it installs)
5960
> work with almost nothing else installed, making it ideal for tiny containers.
6061

crates/cli/src/execve.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
#[cfg(unix)]
12
use nix::unistd::execve as nix_execve;
3+
#[cfg(unix)]
24
use std::ffi::CString;
5+
36
use std::{collections::HashMap, error::Error};
7+
use libpkgx::env::PlatformCaseAwareEnvKey;
48

9+
#[cfg(unix)]
510
pub fn execve(
611
cmd: String,
712
mut args: Vec<String>,
8-
env: HashMap<String, String>,
13+
env: HashMap<PlatformCaseAwareEnvKey, String>,
914
) -> Result<(), Box<dyn Error>> {
1015
// Convert the command to a CString
16+
1117
let c_command = CString::new(cmd.clone())
1218
.map_err(|e| format!("Failed to convert command to CString: {}", e))?;
1319

@@ -47,3 +53,21 @@ pub fn execve(
4753

4854
Ok(())
4955
}
56+
57+
#[cfg(windows)]
58+
use std::process::{exit, Command};
59+
60+
#[cfg(windows)]
61+
pub fn execve(
62+
cmd: String,
63+
args: Vec<String>,
64+
env: HashMap<PlatformCaseAwareEnvKey, String>,
65+
) -> Result<(), Box<dyn Error>> {
66+
let status = Command::new(cmd)
67+
.args(args)
68+
.envs(env.iter().map(|(k, v)| (&k.0, v)))
69+
.spawn()?
70+
.wait()?;
71+
72+
exit(status.code().unwrap_or(1));
73+
}

crates/cli/src/main.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{collections::HashMap, error::Error, fmt::Write, sync::Arc, time::Durat
99
use execve::execve;
1010
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
1111
use libpkgx::{
12-
config::Config, env, hydrate::hydrate, install_multi, pantry_db, resolve::resolve, sync,
12+
config::Config, env::{self, construct_platform_case_aware_env_key}, hydrate::hydrate, install_multi, pantry_db, resolve::resolve, sync,
1313
types::PackageReq, utils,
1414
};
1515
use regex::Regex;
@@ -195,9 +195,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
195195
paths.append(&mut pkgpaths.clone());
196196
}
197197
if let Ok(syspaths) = std::env::var("PATH") {
198+
#[cfg(windows)]
199+
let sep = ";";
200+
#[cfg(not(windows))]
201+
let sep = ":";
198202
paths.extend(
199203
syspaths
200-
.split(':')
204+
.split(sep)
201205
.map(|x| x.to_string())
202206
.collect::<Vec<String>>(),
203207
);
@@ -223,7 +227,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
223227
}
224228

225229
// fork bomb protection
226-
env.insert("PKGX_LVL".to_string(), pkgx_lvl.to_string());
230+
env.insert(construct_platform_case_aware_env_key("PKGX_LVL".to_string()), pkgx_lvl.to_string());
227231

228232
clear_progress_bar();
229233

@@ -232,7 +236,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
232236
clear_progress_bar();
233237

234238
if !flags.json {
235-
let env = env.iter().map(|(k, v)| (k.clone(), v.join(":"))).collect();
239+
let env = env.iter().map(|(k, v)| (construct_platform_case_aware_env_key(k.clone()), v.join(":"))).collect();
236240
let env = env::mix_runtime(&env, &installations, &conn)?;
237241
for (key, value) in env {
238242
println!(

crates/lib/src/env.rs

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,66 @@ use std::{
55
str::FromStr,
66
};
77

8+
#[cfg(windows)]
9+
use std::{ hash::{Hash, Hasher}, fmt};
10+
11+
#[cfg(windows)]
12+
#[derive(Clone)]
13+
pub struct CaseInsensitiveKey(pub String);
14+
15+
#[cfg(windows)]
16+
impl PartialEq for CaseInsensitiveKey {
17+
fn eq(&self, other: &Self) -> bool {
18+
self.0.eq_ignore_ascii_case(&other.0)
19+
}
20+
}
21+
22+
#[cfg(windows)]
23+
impl fmt::Display for CaseInsensitiveKey {
24+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25+
write!(f, "{}", self.0)
26+
}
27+
}
28+
29+
#[cfg(windows)]
30+
impl Eq for CaseInsensitiveKey {}
31+
32+
#[cfg(windows)]
33+
impl Hash for CaseInsensitiveKey {
34+
fn hash<H: Hasher>(&self, state: &mut H) {
35+
self.0.to_lowercase().hash(state);
36+
}
37+
}
38+
39+
#[cfg(windows)]
40+
impl fmt::Debug for CaseInsensitiveKey {
41+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42+
write!(f, "{:?}", self.0)
43+
}
44+
}
45+
46+
#[cfg(windows)]
47+
pub type PlatformCaseAwareEnvKey = CaseInsensitiveKey;
48+
#[cfg(not(windows))]
49+
pub type PlatformCaseAwareEnvKey = String;
50+
51+
#[cfg(windows)]
52+
pub fn construct_platform_case_aware_env_key(key: String) -> PlatformCaseAwareEnvKey {
53+
CaseInsensitiveKey(key)
54+
}
55+
56+
#[cfg(not(windows))]
57+
pub fn construct_platform_case_aware_env_key(key: String) -> PlatformCaseAwareEnvKey {
58+
key
59+
}
60+
861
use crate::types::Installation;
962

63+
#[cfg(not(windows))]
64+
const SEP: &str = ":";
65+
#[cfg(windows)]
66+
const SEP: &str = ";";
67+
1068
pub fn map(installations: &Vec<Installation>) -> HashMap<String, Vec<String>> {
1169
let mut vars: HashMap<EnvKey, OrderedSet<PathBuf>> = HashMap::new();
1270

@@ -42,7 +100,9 @@ pub fn map(installations: &Vec<Installation>) -> HashMap<String, Vec<String>> {
42100
.unwrap()
43101
.add(PathBuf::from_str("/usr/share/man").unwrap());
44102
}
103+
45104
// https://github.com/pkgxdev/libpkgx/issues/70
105+
#[cfg(unix)]
46106
if vars.contains_key(&EnvKey::XdgDataDirs) {
47107
let set = vars.get_mut(&EnvKey::XdgDataDirs).unwrap();
48108
set.add(PathBuf::from_str("/usr/local/share").unwrap());
@@ -71,17 +131,25 @@ enum EnvKey {
71131
Path,
72132
Manpath,
73133
PkgConfigPath,
134+
#[cfg(unix)]
74135
LibraryPath,
136+
#[cfg(unix)]
75137
LdLibraryPath,
138+
#[cfg(unix)]
76139
Cpath,
77140
XdgDataDirs,
78141
CmakePrefixPath,
79142
#[cfg(target_os = "macos")]
80143
DyldFallbackLibraryPath,
81144
SslCertFile,
145+
#[cfg(unix)]
82146
Ldflags,
83147
PkgxDir,
84148
AclocalPath,
149+
#[cfg(windows)]
150+
Lib,
151+
#[cfg(windows)]
152+
Include,
85153
}
86154

87155
struct OrderedSet<T: Eq + std::hash::Hash + Clone> {
@@ -111,44 +179,58 @@ fn suffixes(key: &EnvKey) -> Option<Vec<&'static str>> {
111179
EnvKey::PkgConfigPath => Some(vec!["share/pkgconfig", "lib/pkgconfig"]),
112180
EnvKey::XdgDataDirs => Some(vec!["share"]),
113181
EnvKey::AclocalPath => Some(vec!["share/aclocal"]),
182+
#[cfg(unix)]
114183
EnvKey::LibraryPath | EnvKey::LdLibraryPath => Some(vec!["lib", "lib64"]),
115184
#[cfg(target_os = "macos")]
116185
EnvKey::DyldFallbackLibraryPath => Some(vec!["lib", "lib64"]),
186+
#[cfg(unix)]
117187
EnvKey::Cpath => Some(vec!["include"]),
118-
EnvKey::CmakePrefixPath | EnvKey::SslCertFile | EnvKey::Ldflags | EnvKey::PkgxDir => None,
188+
EnvKey::CmakePrefixPath | EnvKey::SslCertFile | EnvKey::PkgxDir => None,
189+
#[cfg(unix)]
190+
EnvKey::Ldflags => None,
191+
#[cfg(windows)]
192+
EnvKey::Lib => Some(vec!["lib"]),
193+
#[cfg(windows)]
194+
EnvKey::Include => Some(vec!["include"]),
119195
}
120196
}
121197

122-
pub fn mix(input: HashMap<String, Vec<String>>) -> HashMap<String, String> {
123-
let mut rv = HashMap::from_iter(std::env::vars());
198+
pub fn mix(input: HashMap<String, Vec<String>>) -> HashMap<PlatformCaseAwareEnvKey, String> {
199+
let mut rv: HashMap<PlatformCaseAwareEnvKey, String> = HashMap::new();
200+
201+
for (key, value) in std::env::vars() {
202+
rv.insert(construct_platform_case_aware_env_key(key), value);
203+
}
124204

125205
for (key, value) in input.iter() {
206+
let key = &construct_platform_case_aware_env_key(key.clone());
126207
if let Some(values) = rv.get(key) {
127-
rv.insert(key.clone(), format!("{}:{}", value.join(":"), values));
208+
rv.insert(key.clone(), format!("{}{}{}", value.join(SEP), SEP, values));
128209
} else {
129-
rv.insert(key.clone(), value.join(":"));
210+
rv.insert(key.clone(), value.join(SEP));
130211
}
131212
}
132213

133214
rv
134215
}
135216

136217
pub fn mix_runtime(
137-
input: &HashMap<String, String>,
218+
input: &HashMap<PlatformCaseAwareEnvKey, String>,
138219
installations: &Vec<Installation>,
139220
conn: &Connection,
140-
) -> Result<HashMap<String, String>, Box<dyn Error>> {
141-
let mut output: HashMap<String, String> = input
221+
) -> Result<HashMap<PlatformCaseAwareEnvKey, String>, Box<dyn Error>> {
222+
let mut output: HashMap<PlatformCaseAwareEnvKey, String> = input
142223
.iter()
143-
.map(|(k, v)| (k.clone(), format!("{}:${}", v, k)))
224+
.map(|(k, v)| (k.clone(), format!("{}{}${}", v, SEP, k)))
144225
.collect();
145226

146227
for installation in installations.clone() {
147228
let runtime_env =
148229
crate::pantry_db::runtime_env_for_project(&installation.pkg.project, conn)?;
149230
for (key, runtime_value) in runtime_env {
150231
let runtime_value = expand_moustaches(&runtime_value, &installation, installations);
151-
let new_value = if let Some(curr_value) = output.get(&key) {
232+
let insert_key = construct_platform_case_aware_env_key(key.clone());
233+
let new_value = if let Some(curr_value) = output.get(&insert_key) {
152234
if runtime_value.contains(&format!("${}", key)) {
153235
runtime_value.replace(&format!("${}", key), curr_value)
154236
} else {
@@ -161,7 +243,7 @@ pub fn mix_runtime(
161243
} else {
162244
format!("${{{}:-{}}}", key, runtime_value)
163245
};
164-
output.insert(key, new_value);
246+
output.insert(insert_key, new_value);
165247
}
166248
}
167249

crates/lib/src/install.rs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use async_compression::tokio::bufread::XzDecoder;
22
use fs2::FileExt;
3-
use std::{error::Error, fs::OpenOptions, path::PathBuf};
3+
use std::{
4+
error::Error,
5+
fs::{self, OpenOptions},
6+
};
47
use tempfile::tempdir_in;
58
use tokio::task;
69
use tokio_tar::Archive;
@@ -38,14 +41,23 @@ where
3841
{
3942
let shelf = config.pkgx_dir.join(&pkg.project);
4043
fs::create_dir_all(&shelf)?;
41-
let shelf = OpenOptions::new()
44+
45+
#[cfg(windows)]
46+
let lockfile = OpenOptions::new()
47+
.read(true)
48+
.write(true)
49+
.create(true)
50+
.truncate(true)
51+
.open(shelf.join("lockfile"))?;
52+
#[cfg(not(windows))]
53+
let lockfile = OpenOptions::new()
4254
.read(true) // Open the directory in read-only mode
4355
.open(shelf.clone())?;
4456

4557
task::spawn_blocking({
46-
let shelf = shelf.try_clone()?;
58+
let lockfile = lockfile.try_clone()?;
4759
move || {
48-
shelf
60+
lockfile
4961
.lock_exclusive()
5062
.expect("unexpected error: install locking failed");
5163
}
@@ -57,7 +69,7 @@ where
5769
// did another instance of pkgx install us while we waited for the lock?
5870
// if so, we’re good: eject
5971
if dst_path.is_dir() {
60-
FileExt::unlock(&shelf)?;
72+
FileExt::unlock(&lockfile)?;
6173
return Ok(Installation {
6274
path: dst_path,
6375
pkg: pkg.clone(),
@@ -110,19 +122,22 @@ where
110122
pkg: pkg.clone(),
111123
};
112124

125+
#[cfg(not(windows))]
113126
symlink(&installation, config).await?;
127+
// ^^ you need admin privs to symlink on windows (wtf)
114128

115-
FileExt::unlock(&shelf)?;
129+
FileExt::unlock(&lockfile)?;
116130

117131
Ok(installation)
118132
}
119133

120-
use libsemverator::range::Range as VersionReq;
121-
use libsemverator::semver::Semver as Version;
122-
use std::collections::VecDeque;
123-
use std::fs;
124-
use std::path::Path;
134+
#[cfg(not(windows))]
135+
use {
136+
libsemverator::range::Range as VersionReq, libsemverator::semver::Semver as Version,
137+
std::collections::VecDeque, std::path::Path, std::path::PathBuf,
138+
};
125139

140+
#[cfg(not(windows))]
126141
async fn symlink(installation: &Installation, config: &Config) -> Result<(), Box<dyn Error>> {
127142
let mut versions: VecDeque<(Version, PathBuf)> = cellar::ls(&installation.pkg.project, config)
128143
.await?
@@ -183,6 +198,7 @@ async fn symlink(installation: &Installation, config: &Config) -> Result<(), Box
183198
Ok(())
184199
}
185200

201+
#[cfg(not(windows))]
186202
async fn make_symlink(
187203
shelf: &Path,
188204
symname: &str,
@@ -203,9 +219,10 @@ async fn make_symlink(
203219
.file_name()
204220
.ok_or_else(|| anyhow::anyhow!("Could not get the base name of the installation path"))?;
205221

206-
match std::os::unix::fs::symlink(target, &symlink_path) {
207-
Ok(_) => Ok(()),
208-
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
209-
Err(err) => Err(err.into()),
210-
}
222+
#[cfg(not(windows))]
223+
std::os::unix::fs::symlink(target, &symlink_path)?;
224+
#[cfg(windows)]
225+
std::os::windows::fs::symlink_dir(target, symlink_path)?;
226+
227+
Ok(())
211228
}

0 commit comments

Comments
 (0)