Skip to content

Commit 794a438

Browse files
committed
Add FUSE backend compatibility spike workflow
Tests fusepy + FUSE backend on all 3 CI platforms: - macOS: FUSE-T (brew install fuse-t) - Linux: fuse3 (apt install fuse3) - Windows: WinFSP (choco install winfsp) Each job verifies fusepy import and attempts a minimal NullFS mount to confirm end-to-end compatibility.
1 parent c2eca7d commit 794a438

1 file changed

Lines changed: 318 additions & 0 deletions

File tree

.github/workflows/fuse-t-spike.yml

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
name: FUSE Backend Spike
2+
3+
on:
4+
push:
5+
branches: [spike/fuse-t-compat]
6+
workflow_dispatch:
7+
8+
jobs:
9+
macos-fuse-t:
10+
name: macOS (FUSE-T)
11+
runs-on: macos-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
submodules: true
16+
17+
- name: Set up Python 3.13
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.13"
21+
22+
- name: Install FUSE-T
23+
run: brew install fuse-t
24+
25+
- name: Verify FUSE-T library
26+
run: |
27+
ls /usr/local/lib/libfuse-t.dylib 2>/dev/null \
28+
|| ls /opt/homebrew/lib/libfuse-t.dylib 2>/dev/null \
29+
|| echo "FUSE-T lib not in standard paths"
30+
31+
- name: Install fusepy
32+
run: pip install fusepy
33+
34+
- name: Test fusepy import
35+
run: |
36+
python3 -c "
37+
try:
38+
from fuse import FUSE, Operations
39+
print('SUCCESS: fusepy imported with FUSE-T backend')
40+
except EnvironmentError as e:
41+
print(f'FAIL (EnvironmentError): {e}')
42+
exit(1)
43+
except Exception as e:
44+
print(f'FAIL ({type(e).__name__}): {e}')
45+
exit(1)
46+
"
47+
48+
- name: Test NullFS mount
49+
run: |
50+
python3 << 'PYEOF'
51+
import os, subprocess, sys, tempfile, time
52+
53+
mp = tempfile.mkdtemp(prefix='fuset_spike_')
54+
print(f'Mountpoint: {mp}')
55+
56+
proc = subprocess.Popen(
57+
[sys.executable, '-c', '''
58+
import os
59+
from fuse import FUSE, Operations
60+
61+
class NullFS(Operations):
62+
def getattr(self, path, fh=None):
63+
if path == '/':
64+
return {'st_mode': 0o40755, 'st_nlink': 2}
65+
raise FileNotFoundError(path)
66+
def readdir(self, path, fh):
67+
return ['.', '..']
68+
69+
mp = os.environ['SPIKE_MP']
70+
print(f'Mounting NullFS at {mp}', flush=True)
71+
FUSE(NullFS(), mp, foreground=True)
72+
'''],
73+
env={**os.environ, 'SPIKE_MP': mp},
74+
stdout=subprocess.PIPE,
75+
stderr=subprocess.PIPE,
76+
)
77+
78+
time.sleep(5)
79+
80+
mounted = os.path.ismount(mp)
81+
print(f'os.path.ismount({mp}) = {mounted}')
82+
83+
try:
84+
entries = os.listdir(mp)
85+
print(f'os.listdir({mp}) = {entries}')
86+
listdir_ok = True
87+
except OSError as e:
88+
print(f'os.listdir failed: {e}')
89+
listdir_ok = False
90+
91+
if mounted:
92+
subprocess.run(['umount', mp], check=False)
93+
proc.terminate()
94+
try:
95+
stdout, stderr = proc.communicate(timeout=10)
96+
print(f'Mount process stdout: {stdout.decode(errors="replace")}')
97+
print(f'Mount process stderr: {stderr.decode(errors="replace")}')
98+
except subprocess.TimeoutExpired:
99+
proc.kill()
100+
proc.communicate()
101+
102+
try:
103+
os.rmdir(mp)
104+
except OSError:
105+
pass
106+
107+
if mounted or listdir_ok:
108+
print('RESULT: FUSE-T + fusepy COMPATIBLE')
109+
else:
110+
print('RESULT: FUSE-T + fusepy INCOMPATIBLE')
111+
sys.exit(1)
112+
PYEOF
113+
114+
linux-fuse3:
115+
name: Linux (fuse3)
116+
runs-on: ubuntu-latest
117+
steps:
118+
- uses: actions/checkout@v4
119+
with:
120+
submodules: true
121+
122+
- name: Set up Python 3.13
123+
uses: actions/setup-python@v5
124+
with:
125+
python-version: "3.13"
126+
127+
- name: Install fuse3
128+
run: |
129+
sudo apt-get update
130+
sudo apt-get install -y fuse3 libfuse3-dev
131+
sudo modprobe fuse || true
132+
133+
- name: Install fusepy
134+
run: pip install fusepy
135+
136+
- name: Test fusepy import
137+
run: |
138+
python3 -c "
139+
try:
140+
from fuse import FUSE, Operations
141+
print('SUCCESS: fusepy imported with fuse3 backend')
142+
except EnvironmentError as e:
143+
print(f'FAIL (EnvironmentError): {e}')
144+
exit(1)
145+
except Exception as e:
146+
print(f'FAIL ({type(e).__name__}): {e}')
147+
exit(1)
148+
"
149+
150+
- name: Test NullFS mount
151+
run: |
152+
python3 << 'PYEOF'
153+
import os, subprocess, sys, tempfile, time
154+
155+
mp = tempfile.mkdtemp(prefix='fuse3_spike_')
156+
print(f'Mountpoint: {mp}')
157+
158+
proc = subprocess.Popen(
159+
[sys.executable, '-c', '''
160+
import os
161+
from fuse import FUSE, Operations
162+
163+
class NullFS(Operations):
164+
def getattr(self, path, fh=None):
165+
if path == '/':
166+
return {'st_mode': 0o40755, 'st_nlink': 2}
167+
raise FileNotFoundError(path)
168+
def readdir(self, path, fh):
169+
return ['.', '..']
170+
171+
mp = os.environ['SPIKE_MP']
172+
print(f'Mounting NullFS at {mp}', flush=True)
173+
FUSE(NullFS(), mp, foreground=True)
174+
'''],
175+
env={**os.environ, 'SPIKE_MP': mp},
176+
stdout=subprocess.PIPE,
177+
stderr=subprocess.PIPE,
178+
)
179+
180+
time.sleep(5)
181+
182+
mounted = os.path.ismount(mp)
183+
print(f'os.path.ismount({mp}) = {mounted}')
184+
185+
try:
186+
entries = os.listdir(mp)
187+
print(f'os.listdir({mp}) = {entries}')
188+
listdir_ok = True
189+
except OSError as e:
190+
print(f'os.listdir failed: {e}')
191+
listdir_ok = False
192+
193+
if mounted:
194+
subprocess.run(['fusermount3', '-u', mp], check=False)
195+
proc.terminate()
196+
try:
197+
stdout, stderr = proc.communicate(timeout=10)
198+
print(f'Mount process stdout: {stdout.decode(errors="replace")}')
199+
print(f'Mount process stderr: {stderr.decode(errors="replace")}')
200+
except subprocess.TimeoutExpired:
201+
proc.kill()
202+
proc.communicate()
203+
204+
try:
205+
os.rmdir(mp)
206+
except OSError:
207+
pass
208+
209+
if mounted or listdir_ok:
210+
print('RESULT: fuse3 + fusepy COMPATIBLE')
211+
else:
212+
print('RESULT: fuse3 + fusepy INCOMPATIBLE')
213+
sys.exit(1)
214+
PYEOF
215+
216+
windows-winfsp:
217+
name: Windows (WinFSP)
218+
runs-on: windows-latest
219+
steps:
220+
- uses: actions/checkout@v4
221+
with:
222+
submodules: true
223+
224+
- name: Set up Python 3.13
225+
uses: actions/setup-python@v5
226+
with:
227+
python-version: "3.13"
228+
229+
- name: Install WinFSP
230+
run: choco install winfsp -y
231+
232+
- name: Install fusepy
233+
run: pip install fusepy
234+
235+
- name: Test fusepy import
236+
shell: python
237+
run: |
238+
try:
239+
from fuse import FUSE, Operations
240+
print('SUCCESS: fusepy imported with WinFSP backend')
241+
except EnvironmentError as e:
242+
print(f'FAIL (EnvironmentError): {e}')
243+
exit(1)
244+
except Exception as e:
245+
print(f'FAIL ({type(e).__name__}): {e}')
246+
exit(1)
247+
248+
- name: Test NullFS mount
249+
shell: python
250+
run: |
251+
import os, string, subprocess, sys, time
252+
253+
# Find an unused drive letter
254+
used = set()
255+
for d in string.ascii_uppercase:
256+
if os.path.exists(f'{d}:\\'):
257+
used.add(d)
258+
drive = None
259+
for d in reversed(string.ascii_uppercase):
260+
if d not in used:
261+
drive = d
262+
break
263+
if not drive:
264+
print('No free drive letter')
265+
sys.exit(1)
266+
267+
mp = f'{drive}:\\'
268+
print(f'Mountpoint: {mp}')
269+
270+
proc = subprocess.Popen(
271+
[sys.executable, '-c', '''
272+
import os
273+
from fuse import FUSE, Operations
274+
275+
class NullFS(Operations):
276+
def getattr(self, path, fh=None):
277+
if path in ('/', '\\\\'):
278+
return {'st_mode': 0o40755, 'st_nlink': 2}
279+
raise FileNotFoundError(path)
280+
def readdir(self, path, fh):
281+
return ['.', '..']
282+
283+
mp = os.environ['SPIKE_MP']
284+
print(f'Mounting NullFS at {mp}', flush=True)
285+
FUSE(NullFS(), mp, foreground=True)
286+
'''],
287+
env={**os.environ, 'SPIKE_MP': mp},
288+
stdout=subprocess.PIPE,
289+
stderr=subprocess.PIPE,
290+
)
291+
292+
time.sleep(10)
293+
294+
mounted = os.path.exists(mp)
295+
print(f'os.path.exists({mp}) = {mounted}')
296+
297+
try:
298+
entries = os.listdir(mp)
299+
print(f'os.listdir({mp}) = {entries}')
300+
listdir_ok = True
301+
except OSError as e:
302+
print(f'os.listdir failed: {e}')
303+
listdir_ok = False
304+
305+
proc.terminate()
306+
try:
307+
stdout, stderr = proc.communicate(timeout=10)
308+
print(f'Mount process stdout: {stdout.decode(errors="replace")}')
309+
print(f'Mount process stderr: {stderr.decode(errors="replace")}')
310+
except subprocess.TimeoutExpired:
311+
proc.kill()
312+
proc.communicate()
313+
314+
if listdir_ok:
315+
print('RESULT: WinFSP + fusepy COMPATIBLE')
316+
else:
317+
print('RESULT: WinFSP + fusepy INCOMPATIBLE')
318+
sys.exit(1)

0 commit comments

Comments
 (0)