huskies: merge 1110 story Chat bootstrap Phase 2b: additional stack overlays (Go, Python, Ruby, JVM)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# Go stack overlay fragment.
|
||||
#
|
||||
# Layer this on top of huskies-project-base to produce a project container
|
||||
# with Go 1.22, gopls (official Go language server), and standard tooling.
|
||||
#
|
||||
# Build the combined image:
|
||||
# (echo "FROM huskies-project-base"; \
|
||||
# cat docker/stacks/go/Dockerfile.fragment) | \
|
||||
# docker build -t huskies-project-go -
|
||||
#
|
||||
# Adding a new stack: create docker/stacks/<name>/Dockerfile.fragment and
|
||||
# docker/stacks/<name>/markers — no changes to orchestration code required.
|
||||
|
||||
USER root
|
||||
|
||||
# Official Go binary distribution — Debian's golang-go package is too old for gopls.
|
||||
# Update GOVERSION to pick up a newer release.
|
||||
ENV GOVERSION="1.22.3"
|
||||
RUN curl -fsSL "https://go.dev/dl/go${GOVERSION}.linux-amd64.tar.gz" \
|
||||
| tar -C /usr/local -xzf -
|
||||
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
|
||||
# gopls: the official Go language server.
|
||||
# GOBIN=/usr/local/bin puts the binary on the system PATH for all users.
|
||||
RUN GOBIN=/usr/local/bin go install golang.org/x/tools/gopls@latest
|
||||
|
||||
USER huskies
|
||||
@@ -0,0 +1,4 @@
|
||||
# Stack detection markers for the go stack.
|
||||
# Each non-blank, non-comment line names a file relative to the project root.
|
||||
# If any listed file exists in the project directory, this stack is matched.
|
||||
go.mod
|
||||
@@ -0,0 +1,50 @@
|
||||
# JVM stack overlay fragment.
|
||||
#
|
||||
# Layer this on top of huskies-project-base to produce a project container
|
||||
# with OpenJDK 21, Maven, and eclipse.jdt.ls (the canonical Java/JVM LSP).
|
||||
#
|
||||
# Build the combined image:
|
||||
# (echo "FROM huskies-project-base"; \
|
||||
# cat docker/stacks/jvm/Dockerfile.fragment) | \
|
||||
# docker build -t huskies-project-jvm -
|
||||
#
|
||||
# Adding a new stack: create docker/stacks/<name>/Dockerfile.fragment and
|
||||
# docker/stacks/<name>/markers — no changes to orchestration code required.
|
||||
|
||||
USER root
|
||||
|
||||
# OpenJDK 21 (current LTS) and Maven for build support.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openjdk-21-jdk-headless \
|
||||
maven \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64"
|
||||
|
||||
# Eclipse JDT Language Server — canonical LSP for Java/JVM (Java, Kotlin, Groovy).
|
||||
# Pin to a specific release; update JDTLS_VERSION + JDTLS_BUILD for upgrades.
|
||||
# All releases: https://github.com/eclipse-jdtls/eclipse.jdt.ls/releases
|
||||
ENV JDTLS_VERSION="1.38.0" \
|
||||
JDTLS_BUILD="202503271418"
|
||||
RUN mkdir -p /opt/jdtls \
|
||||
&& curl -fsSL \
|
||||
"https://download.eclipse.org/jdtls/milestones/${JDTLS_VERSION}/jdt-language-server-${JDTLS_VERSION}-${JDTLS_BUILD}.tar.gz" \
|
||||
| tar -xzf - -C /opt/jdtls
|
||||
|
||||
# Wrapper script so `jdtls` is available as a PATH command.
|
||||
RUN { \
|
||||
echo '#!/bin/sh'; \
|
||||
echo 'JAR=$(ls /opt/jdtls/plugins/org.eclipse.equinox.launcher_*.jar 2>/dev/null | head -1)'; \
|
||||
echo 'exec java \'; \
|
||||
echo ' -Declipse.application=org.eclipse.jdt.ls.core.id1 \'; \
|
||||
echo ' -Dosgi.bundles.defaultStartLevel=4 \'; \
|
||||
echo ' -Declipse.product=org.eclipse.jdt.ls.core.product \'; \
|
||||
echo ' -Dlog.protocol=true \'; \
|
||||
echo ' -Dlog.level=ALL \'; \
|
||||
echo ' -jar "$JAR" \'; \
|
||||
echo ' -configuration /opt/jdtls/config_linux \'; \
|
||||
echo ' "$@"'; \
|
||||
} > /usr/local/bin/jdtls \
|
||||
&& chmod +x /usr/local/bin/jdtls
|
||||
|
||||
USER huskies
|
||||
@@ -0,0 +1,6 @@
|
||||
# Stack detection markers for the jvm stack.
|
||||
# Each non-blank, non-comment line names a file relative to the project root.
|
||||
# If any listed file exists in the project directory, this stack is matched.
|
||||
pom.xml
|
||||
build.gradle
|
||||
build.gradle.kts
|
||||
@@ -0,0 +1,27 @@
|
||||
# Python stack overlay fragment.
|
||||
#
|
||||
# Layer this on top of huskies-project-base to produce a project container
|
||||
# with Python 3, pip, and pyright (the Microsoft Python LSP / type checker).
|
||||
#
|
||||
# Build the combined image:
|
||||
# (echo "FROM huskies-project-base"; \
|
||||
# cat docker/stacks/python/Dockerfile.fragment) | \
|
||||
# docker build -t huskies-project-python -
|
||||
#
|
||||
# Adding a new stack: create docker/stacks/<name>/Dockerfile.fragment and
|
||||
# docker/stacks/<name>/markers — no changes to orchestration code required.
|
||||
|
||||
USER root
|
||||
|
||||
# Python 3 runtime and pip.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# pyright: Microsoft's Python language server / static type checker.
|
||||
# --break-system-packages is required on Debian 12+ where pip is externally
|
||||
# managed; the flag is safe inside a Docker container.
|
||||
RUN pip install --no-cache-dir --break-system-packages pyright
|
||||
|
||||
USER huskies
|
||||
@@ -0,0 +1,6 @@
|
||||
# Stack detection markers for the python stack.
|
||||
# Each non-blank, non-comment line names a file relative to the project root.
|
||||
# If any listed file exists in the project directory, this stack is matched.
|
||||
pyproject.toml
|
||||
requirements.txt
|
||||
setup.py
|
||||
@@ -0,0 +1,28 @@
|
||||
# Ruby stack overlay fragment.
|
||||
#
|
||||
# Layer this on top of huskies-project-base to produce a project container
|
||||
# with Ruby, Bundler, and ruby-lsp (the Shopify Ruby language server).
|
||||
#
|
||||
# Build the combined image:
|
||||
# (echo "FROM huskies-project-base"; \
|
||||
# cat docker/stacks/ruby/Dockerfile.fragment) | \
|
||||
# docker build -t huskies-project-ruby -
|
||||
#
|
||||
# Adding a new stack: create docker/stacks/<name>/Dockerfile.fragment and
|
||||
# docker/stacks/<name>/markers — no changes to orchestration code required.
|
||||
|
||||
USER root
|
||||
|
||||
# Ruby runtime, development headers (needed by native gem extensions), and Bundler.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ruby \
|
||||
ruby-dev \
|
||||
bundler \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ruby-lsp: Shopify's Ruby language server (LSP-compliant, actively maintained).
|
||||
# Installed globally so the `ruby-lsp` binary is available on PATH.
|
||||
RUN gem install ruby-lsp
|
||||
|
||||
USER huskies
|
||||
@@ -0,0 +1,4 @@
|
||||
# Stack detection markers for the ruby stack.
|
||||
# Each non-blank, non-comment line names a file relative to the project root.
|
||||
# If any listed file exists in the project directory, this stack is matched.
|
||||
Gemfile
|
||||
@@ -97,7 +97,8 @@ pub fn detect_stack(project_path: &Path, stacks_dir: &Path) -> (Option<String>,
|
||||
Err(_) => return (None, vec![]),
|
||||
};
|
||||
|
||||
let mut matched: Vec<String> = Vec::new();
|
||||
// (stack_name, number_of_matched_marker_files)
|
||||
let mut matched: Vec<(String, usize)> = Vec::new();
|
||||
|
||||
let mut stack_dirs: Vec<_> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
@@ -112,26 +113,32 @@ pub fn detect_stack(project_path: &Path, stacks_dir: &Path) -> (Option<String>,
|
||||
let Ok(content) = std::fs::read_to_string(&markers_path) else {
|
||||
continue;
|
||||
};
|
||||
let hit = content.lines().any(|line| {
|
||||
let count = content
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
return false;
|
||||
}
|
||||
project_path.join(trimmed).exists()
|
||||
});
|
||||
if hit {
|
||||
matched.push(stack_name);
|
||||
!trimmed.is_empty()
|
||||
&& !trimmed.starts_with('#')
|
||||
&& project_path.join(trimmed).exists()
|
||||
})
|
||||
.count();
|
||||
if count > 0 {
|
||||
matched.push((stack_name, count));
|
||||
}
|
||||
}
|
||||
|
||||
match matched.len() {
|
||||
0 => (None, vec![]),
|
||||
1 => (Some(matched.remove(0)), vec![]),
|
||||
1 => (Some(matched.remove(0).0), vec![]),
|
||||
_ => {
|
||||
let names = matched.join(", ");
|
||||
let chosen = matched.remove(0);
|
||||
// Dominant stack: most marker files matched; alphabetical tiebreak for stability.
|
||||
matched.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
let names: Vec<String> = matched.iter().map(|(n, _)| n.clone()).collect();
|
||||
let names_str = names.join(", ");
|
||||
let chosen = matched.swap_remove(0).0;
|
||||
let warning = format!(
|
||||
"Multiple stacks detected ({names}); using **{chosen}**. \
|
||||
"Multiple stacks detected ({names_str}); using **{chosen}** \
|
||||
(most marker files matched). \
|
||||
Pass `--stack <name>` to override."
|
||||
);
|
||||
(Some(chosen), vec![warning])
|
||||
@@ -527,4 +534,171 @@ mod tests {
|
||||
assert_eq!(stack, None);
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_go_mod() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("go.mod"), "module example").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("go")).unwrap();
|
||||
std::fs::write(stacks.path().join("go/markers"), "go.mod\n").unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
assert_eq!(stack, Some("go".to_string()));
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_python_pyproject() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("pyproject.toml"), "[tool.poetry]").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("python")).unwrap();
|
||||
std::fs::write(
|
||||
stacks.path().join("python/markers"),
|
||||
"pyproject.toml\nrequirements.txt\nsetup.py\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
assert_eq!(stack, Some("python".to_string()));
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_python_requirements_txt() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("requirements.txt"), "flask\n").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("python")).unwrap();
|
||||
std::fs::write(
|
||||
stacks.path().join("python/markers"),
|
||||
"pyproject.toml\nrequirements.txt\nsetup.py\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
assert_eq!(stack, Some("python".to_string()));
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_python_setup_py() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("setup.py"), "from setuptools import setup").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("python")).unwrap();
|
||||
std::fs::write(
|
||||
stacks.path().join("python/markers"),
|
||||
"pyproject.toml\nrequirements.txt\nsetup.py\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
assert_eq!(stack, Some("python".to_string()));
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_ruby_gemfile() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("Gemfile"), "source 'https://rubygems.org'").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("ruby")).unwrap();
|
||||
std::fs::write(stacks.path().join("ruby/markers"), "Gemfile\n").unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
assert_eq!(stack, Some("ruby".to_string()));
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_jvm_pom_xml() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("pom.xml"), "<project/>").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("jvm")).unwrap();
|
||||
std::fs::write(
|
||||
stacks.path().join("jvm/markers"),
|
||||
"pom.xml\nbuild.gradle\nbuild.gradle.kts\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
assert_eq!(stack, Some("jvm".to_string()));
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_jvm_build_gradle() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("build.gradle"), "plugins { }").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("jvm")).unwrap();
|
||||
std::fs::write(
|
||||
stacks.path().join("jvm/markers"),
|
||||
"pom.xml\nbuild.gradle\nbuild.gradle.kts\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
assert_eq!(stack, Some("jvm".to_string()));
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_jvm_build_gradle_kts() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("build.gradle.kts"), "plugins { }").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("jvm")).unwrap();
|
||||
std::fs::write(
|
||||
stacks.path().join("jvm/markers"),
|
||||
"pom.xml\nbuild.gradle\nbuild.gradle.kts\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
assert_eq!(stack, Some("jvm".to_string()));
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
/// A polyglot repo with more Python markers than Node markers should prefer python.
|
||||
#[test]
|
||||
fn detect_stack_multiple_dominant_wins() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// Two Python markers: pyproject.toml + requirements.txt
|
||||
std::fs::write(dir.path().join("pyproject.toml"), "").unwrap();
|
||||
std::fs::write(dir.path().join("requirements.txt"), "").unwrap();
|
||||
// One Node marker: package.json (e.g. for a build tool)
|
||||
std::fs::write(dir.path().join("package.json"), "{}").unwrap();
|
||||
|
||||
let stacks = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("node")).unwrap();
|
||||
std::fs::write(
|
||||
stacks.path().join("node/markers"),
|
||||
"package.json\ntsconfig.json\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::create_dir_all(stacks.path().join("python")).unwrap();
|
||||
std::fs::write(
|
||||
stacks.path().join("python/markers"),
|
||||
"pyproject.toml\nrequirements.txt\nsetup.py\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (stack, warnings) = detect_stack(dir.path(), stacks.path());
|
||||
// python matches 2 markers, node matches 1 — python should win.
|
||||
assert_eq!(stack, Some("python".to_string()));
|
||||
assert_eq!(warnings.len(), 1);
|
||||
assert!(warnings[0].contains("Multiple stacks"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user