use std::ffi::OsStr;
use std::path::Path;
use serde_json::{Value, json};
use super::{Result, output::Reporter, process};
const BUILD: &str = include_str!("bridge.py");
fn program() -> String {
format!("{}\n{BUILD}", process::PYTHON_VERSION_CHECK)
}
pub(super) async fn build(
python: &OsStr,
request: Value,
report: &mut Reporter,
) -> Result<process::ChildOutput> {
report.event("warning", json!({"code":"python_extension_bridge","message":"Python extension hooks execute through tmuxp 1.74.x; child output is captured separately from native build events"}))?;
let argv = [python.to_owned(), "-c".into(), program().into()];
let request = request.to_string();
process::run(
&argv,
Path::new(&std::env::current_dir()?),
report,
None,
&[("TMUX_WORKSPACE_BRIDGE_REQUEST", &request)],
)
.await
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn bridge_source_parses_as_python() {
let output = std::process::Command::new("python3")
.args(["-c", "import ast, sys; ast.parse(sys.stdin.read())"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.spawn()
.and_then(|mut child| {
use std::io::Write as _;
child
.stdin
.take()
.expect("stdin was piped")
.write_all(program().as_bytes())?;
child.wait_with_output()
})
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn obsolete_backend_refuses_before_tmuxp_imports() {
let directory = tempfile::tempdir().unwrap();
let library = directory.path().join("libtmux");
let tmuxp = directory.path().join("tmuxp");
std::fs::create_dir(&library).unwrap();
std::fs::create_dir(&tmuxp).unwrap();
std::fs::write(library.join("__init__.py"), "class Server: pass\n").unwrap();
std::fs::write(library.join("common.py"), "TMUX_MIN_VERSION = '3.2a'\n").unwrap();
std::fs::write(
tmuxp.join("__init__.py"),
"from pathlib import Path\nPath('builder-imported').touch()\nraise RuntimeError('builder import ran')\n",
)
.unwrap();
let output = std::process::Command::new("python3")
.args(["-c", &program()])
.current_dir(directory.path())
.env("PYTHONPATH", directory.path())
.output()
.unwrap();
assert!(!directory.path().join("builder-imported").exists());
assert!(!output.status.success(), "{output:?}");
assert!(
String::from_utf8_lossy(&output.stderr).contains("hard tmux minimum of 3.3a"),
"{output:?}"
);
}
#[tokio::test]
#[ignore = "requires TMUX_WORKSPACE_PYTHON with tmuxp 1.74.x"]
async fn append_bridge_rechecks_borrowed_identity_before_imports_and_build() {
let original = libtmux::test::TestServer::new().await.unwrap();
let replacement = libtmux::test::TestServer::new().await.unwrap();
let first = original.session("original").await.unwrap();
let second = replacement.session("replacement").await.unwrap();
assert_eq!(first.id(), second.id());
let identity = original
.server()
.cmd(
libtmux::Command::new("display-message")
.arg("-p")
.arg("-t")
.arg(first.id().to_string())
.arg("#{pid}:#{start_time}"),
)
.await
.unwrap()
.stdout_lossy()
.trim()
.to_owned();
let directory = tempfile::tempdir_in("/tmp/libtmux-rs-test").unwrap();
let alias = directory.path().join("socket,alias");
let config = directory.path().join("workspace.json");
let imported = directory.path().join("imported");
let built = directory.path().join("built");
std::fs::write(
directory.path().join("extension.py"),
r"
import os
from pathlib import Path
from tmuxp.workspace.builder.classic import ClassicWorkspaceBuilder
Path('imported').touch()
class Builder(ClassicWorkspaceBuilder):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if os.environ.get('REPLACEMENT_SOCKET'):
Path('socket,alias').unlink()
Path('socket,alias').symlink_to(os.environ['REPLACEMENT_SOCKET'])
def build(self, *args, **kwargs):
Path('built').touch()
super().build(*args, **kwargs)
",
)
.unwrap();
std::fs::write(&config, json!({"session_name":"unwanted","workspace_builder":"extension:Builder","workspace_builder_paths":[directory.path()],"windows":[{"window_name":"added","panes":["blank"]}]}).to_string()).unwrap();
let python = std::env::var_os("TMUX_WORKSPACE_PYTHON").unwrap();
let mut failures = Vec::new();
for case in ["missing", "mismatch", "constructor"] {
let _ = std::fs::remove_file(&alias);
std::os::unix::fs::symlink(original.socket_path(), &alias).unwrap();
let _ = std::fs::remove_file(&imported);
let _ = std::fs::remove_file(&built);
let request = json!({"path":config,"session_name":"unwanted","socket":alias,
"append":if case == "missing" {"$999".to_owned()} else {first.id().to_string()},
"append_identity":if case == "mismatch" {"0:0"} else {&identity}});
let mut command = std::process::Command::new(&python);
command
.args(["-c", &program()])
.env("TMUX_WORKSPACE_BRIDGE_REQUEST", request.to_string())
.current_dir(directory.path())
.env_remove("TMUX")
.env_remove("TMUX_PANE")
.env_remove("REPLACEMENT_SOCKET");
if case == "constructor" {
command.env("REPLACEMENT_SOCKET", replacement.socket_path());
}
let output = command.output().unwrap();
if output.status.success()
|| built.exists()
|| imported.exists() != (case == "constructor")
|| !String::from_utf8_lossy(&output.stderr).contains("append_context")
{
failures.push(format!(
"{case}: {output:?}; imported={} built={}",
imported.exists(),
built.exists()
));
}
}
for (guard, session) in [(&original, &first), (&replacement, &second)] {
if session.windows().await.unwrap().len() != 1
|| guard.server().sessions().await.unwrap().len() != 1
{
failures.push(format!(
"{} topology changed",
session.name().to_string_lossy()
));
}
}
original.shutdown().await.unwrap();
replacement.shutdown().await.unwrap();
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
}