11import ctypes
22import os
3+ import pprint
34import shutil
45import subprocess
56import tempfile
7+ from pathlib import Path
68
79import pytest
810
911import mfusepy
1012
11- STRUCT_NAMES = ["stat" , "statvfs" , "fuse_file_info" ]
13+ STRUCT_NAMES = {
14+ "stat" : [
15+ 'st_atimespec' ,
16+ 'st_blksize' ,
17+ 'st_blocks' ,
18+ 'st_ctimespec' ,
19+ 'st_dev' ,
20+ 'st_gid' ,
21+ 'st_ino' ,
22+ 'st_mode' ,
23+ 'st_mtimespec' ,
24+ 'st_nlink' ,
25+ 'st_rdev' ,
26+ 'st_size' ,
27+ 'st_uid' ,
28+ ],
29+ "statvfs" : [
30+ 'f_bavail' ,
31+ 'f_bfree' ,
32+ 'f_blocks' ,
33+ 'f_bsize' ,
34+ 'f_favail' ,
35+ 'f_ffree' ,
36+ 'f_files' ,
37+ 'f_flag' ,
38+ 'f_frsize' ,
39+ 'f_fsid' ,
40+ 'f_namemax' ,
41+ ],
42+ "fuse_file_info" : ['flags' , 'fh' , 'lock_owner' ],
43+ }
1244
1345# we only check the struct members that are present on all supported platforms.
1446
15- C_CHECKER = '''
47+ C_CHECKER = r '''
1648#include <stdio.h>
1749#include <stddef.h>
1850#include <sys/stat.h>
1951#include <sys/statvfs.h>
2052#include <fuse.h>
2153
22- int main() {
23- // stat
24- printf("stat_size:%zu\\ n", sizeof(struct stat));
25- printf("stat_st_atimespec_offset:%zu\\ n", offsetof(struct stat, st_atime));
26- printf("stat_st_blksize_offset:%zu\\ n", offsetof(struct stat, st_blksize));
27- printf("stat_st_blocks_offset:%zu\\ n", offsetof(struct stat, st_blocks));
28- printf("stat_st_ctimespec_offset:%zu\\ n", offsetof(struct stat, st_ctime));
29- printf("stat_st_dev_offset:%zu\\ n", offsetof(struct stat, st_dev));
30- printf("stat_st_gid_offset:%zu\\ n", offsetof(struct stat, st_gid));
31- printf("stat_st_ino_offset:%zu\\ n", offsetof(struct stat, st_ino));
32- printf("stat_st_mode_offset:%zu\\ n", offsetof(struct stat, st_mode));
33- printf("stat_st_mtimespec_offset:%zu\\ n", offsetof(struct stat, st_mtime));
34- printf("stat_st_nlink_offset:%zu\\ n", offsetof(struct stat, st_nlink));
35- printf("stat_st_rdev_offset:%zu\\ n", offsetof(struct stat, st_rdev));
36- printf("stat_st_size_offset:%zu\\ n", offsetof(struct stat, st_size));
37- printf("stat_st_uid_offset:%zu\\ n", offsetof(struct stat, st_uid));
38- // statvfs
39- printf("statvfs_size:%zu\\ n", sizeof(struct statvfs));
40- printf("statvfs_f_bavail_offset:%zu\\ n", offsetof(struct statvfs, f_bavail));
41- printf("statvfs_f_bfree_offset:%zu\\ n", offsetof(struct statvfs, f_bfree));
42- printf("statvfs_f_blocks_offset:%zu\\ n", offsetof(struct statvfs, f_blocks));
43- printf("statvfs_f_bsize_offset:%zu\\ n", offsetof(struct statvfs, f_bsize));
44- printf("statvfs_f_favail_offset:%zu\\ n", offsetof(struct statvfs, f_favail));
45- printf("statvfs_f_ffree_offset:%zu\\ n", offsetof(struct statvfs, f_ffree));
46- printf("statvfs_f_files_offset:%zu\\ n", offsetof(struct statvfs, f_files));
47- printf("statvfs_f_flag_offset:%zu\\ n", offsetof(struct statvfs, f_flag));
48- printf("statvfs_f_frsize_offset:%zu\\ n", offsetof(struct statvfs, f_frsize));
49- printf("statvfs_f_fsid_offset:%zu\\ n", offsetof(struct statvfs, f_fsid));
50- printf("statvfs_f_namemax_offset:%zu\\ n", offsetof(struct statvfs, f_namemax));
51- // fuse_file_info - TODO: fix this later!
52- printf("fuse_file_info_size:%zu\\ n", sizeof(struct fuse_file_info));
53- // printf("fuse_file_info_flags_offset:%zu\\ n", offsetof(struct fuse_file_info, flags));
54- // printf("fuse_file_info_fh_offset:%zu\\ n", offsetof(struct fuse_file_info, fh));
55- // printf("fuse_file_info_lock_owner_offset:%zu\\ n", offsetof(struct fuse_file_info, lock_owner));
56- return 0;
57- }
54+ #define PRINT_STAT_MEMBER_OFFSET(NAME) \
55+ printf("stat.%s offset:%zu\n", #NAME, offsetof(struct stat, NAME));
56+
57+ #define PRINT_STATVFS_MEMBER_OFFSET(NAME) \
58+ printf("statvfs.%s offset:%zu\n", #NAME, offsetof(struct statvfs, NAME));
59+
60+ #define PRINT_FUSE_FILE_INFO_MEMBER_OFFSET(NAME) \
61+ printf("fuse_file_info.%s offset:%zu\n", #NAME, offsetof(struct fuse_file_info, NAME));
62+
63+ int main()
64+ {
5865'''
5966
60- py_infos = {
61- # stat
62- 'stat_size' : ctypes .sizeof (mfusepy .c_stat ),
63- 'stat_st_atimespec_offset' : mfusepy .c_stat .st_atimespec .offset ,
64- 'stat_st_blksize_offset' : mfusepy .c_stat .st_blksize .offset ,
65- 'stat_st_blocks_offset' : mfusepy .c_stat .st_blocks .offset ,
66- 'stat_st_ctimespec_offset' : mfusepy .c_stat .st_ctimespec .offset ,
67- 'stat_st_dev_offset' : mfusepy .c_stat .st_dev .offset ,
68- 'stat_st_gid_offset' : mfusepy .c_stat .st_gid .offset ,
69- 'stat_st_ino_offset' : mfusepy .c_stat .st_ino .offset ,
70- 'stat_st_mode_offset' : mfusepy .c_stat .st_mode .offset ,
71- 'stat_st_mtimespec_offset' : mfusepy .c_stat .st_mtimespec .offset ,
72- 'stat_st_nlink_offset' : mfusepy .c_stat .st_nlink .offset ,
73- 'stat_st_rdev_offset' : mfusepy .c_stat .st_rdev .offset ,
74- 'stat_st_size_offset' : mfusepy .c_stat .st_size .offset ,
75- 'stat_st_uid_offset' : mfusepy .c_stat .st_uid .offset ,
76- # statvfs
77- 'statvfs_size' : ctypes .sizeof (mfusepy .c_statvfs ),
78- 'statvfs_f_bavail_offset' : mfusepy .c_statvfs .f_bavail .offset ,
79- 'statvfs_f_bfree_offset' : mfusepy .c_statvfs .f_bfree .offset ,
80- 'statvfs_f_blocks_offset' : mfusepy .c_statvfs .f_blocks .offset ,
81- 'statvfs_f_bsize_offset' : mfusepy .c_statvfs .f_bsize .offset ,
82- 'statvfs_f_favail_offset' : mfusepy .c_statvfs .f_favail .offset ,
83- 'statvfs_f_ffree_offset' : mfusepy .c_statvfs .f_ffree .offset ,
84- 'statvfs_f_files_offset' : mfusepy .c_statvfs .f_files .offset ,
85- 'statvfs_f_flag_offset' : mfusepy .c_statvfs .f_flag .offset ,
86- 'statvfs_f_frsize_offset' : mfusepy .c_statvfs .f_frsize .offset ,
87- 'statvfs_f_fsid_offset' : mfusepy .c_statvfs .f_fsid .offset ,
88- 'statvfs_f_namemax_offset' : mfusepy .c_statvfs .f_namemax .offset ,
89- # fuse_file_info - TODO: fix this later!
90- 'fuse_file_info_size' : ctypes .sizeof (mfusepy .fuse_file_info ),
91- # 'fuse_file_info_flags_offset': mfusepy.fuse_file_info.flags.offset,
92- # 'fuse_file_info_fh_offset': mfusepy.fuse_file_info.fh.offset,
93- # 'fuse_file_info_lock_owner_offset': mfusepy.fuse_file_info.lock_owner.offset,
67+ PY_INFOS = {}
68+ for struct_name , member_names in STRUCT_NAMES .items ():
69+ fusepy_struct = getattr (mfusepy , struct_name , getattr (mfusepy , 'c_' + struct_name , None ))
70+ assert fusepy_struct is not None
71+
72+ PY_INFOS [struct_name + " size" ] = ctypes .sizeof (fusepy_struct )
73+ C_CHECKER += f"""\n printf("{ struct_name } size:%zu\\ n", sizeof(struct { struct_name } ));\n """
74+
75+ for name in member_names :
76+ PY_INFOS [f'{ struct_name } .{ name } offset' ] = getattr (fusepy_struct , name ).offset
77+ # This naming discrepancy is not good but would be an API change, I think.
78+ c_name = name .replace ('timespec' , 'time' ) if name .endswith ('timespec' ) else name
79+ C_CHECKER += f' printf("{ struct_name } .{ name } offset:%zu\\ n", offsetof(struct { struct_name } , { c_name } ));\n '
80+
81+ C_CHECKER += """
82+ return 0;
9483}
84+ """
9585
96- # Common locations for different OSes
97- INCLUDE_PATHS = [
98- '/usr/local/include/fuse' ,
99- '/usr/include/fuse' ,
100- '/usr/local/include/fuse3' ,
101- '/usr/include/fuse3' ,
102- '/usr/local/include/osxfuse/fuse' ,
103- '/usr/local/include/macfuse/fuse' ,
104- '/usr/include/libfuse' ,
105- ]
86+ print (C_CHECKER )
10687
10788
10889def get_compiler ():
@@ -121,19 +102,29 @@ def c_run(name: str, source: str) -> str:
121102 with tempfile .TemporaryDirectory () as tmpdir :
122103 c_file = os .path .join (tmpdir , name + '.c' )
123104 exe_file = os .path .join (tmpdir , name )
105+ preprocessed_file = os .path .join (tmpdir , name + '.preprocessed.c' )
124106
125107 with open (c_file , 'w' , encoding = 'utf-8' ) as f :
126108 f .write (source )
127109
128- # Determine FUSE version and compile flags
129- fuse_major = mfusepy .fuse_version_major
130- fuse_minor = mfusepy .fuse_version_minor
131- print (f"FUSE version: { fuse_major } .{ fuse_minor } " )
110+ print (f"FUSE version: { mfusepy .fuse_version_major } .{ mfusepy .fuse_version_minor } " )
132111
133- cflags = [f'-DFUSE_USE_VERSION={ fuse_major } { fuse_minor } ' , '-D_FILE_OFFSET_BITS=64' ]
112+ # Common include locations for different OSes
113+ include_paths = [
114+ '/usr/local/include/osxfuse/fuse' ,
115+ '/usr/local/include/macfuse/fuse' ,
116+ '/usr/include/libfuse' ,
117+ ]
118+ if mfusepy .fuse_version_major == 3 :
119+ include_paths += ['/usr/local/include/fuse3' , '/usr/include/fuse3' ]
120+ else :
121+ include_paths += ['/usr/local/include/fuse' , '/usr/include/fuse' ]
134122
135- # Try to find fuse headers
136- cflags += [f'-I{ path } ' for path in INCLUDE_PATHS if os .path .exists (path )]
123+ cflags = [
124+ f'-DFUSE_USE_VERSION={ mfusepy .fuse_version_major } { mfusepy .fuse_version_minor } ' ,
125+ '-D_FILE_OFFSET_BITS=64' ,
126+ ]
127+ cflags += [f'-I{ path } ' for path in include_paths if os .path .exists (path )]
137128
138129 # Add possible pkg-config flags if available
139130 for fuse_lib in ("fuse" , "fuse3" ):
@@ -154,35 +145,46 @@ def c_run(name: str, source: str) -> str:
154145 print (f"Compiler stderr:\n { e .stderr } " )
155146 assert e .returncode == 0 , "Could not compile C program to verify sizes."
156147
148+ cmd = [get_compiler (), '-E' , * cflags , c_file , '-o' , preprocessed_file ]
149+ print (f"Preprocessing with: { ' ' .join (cmd )} " )
150+ try :
151+ subprocess .run (cmd , capture_output = True , text = True , check = True )
152+ except subprocess .CalledProcessError as e :
153+ print (f"Compiler return code: { e .returncode } " )
154+ print (f"Compiler stdout:\n { e .stdout } " )
155+ print (f"Compiler stderr:\n { e .stderr } " )
156+ assert e .returncode == 0 , "Could not compile C program to verify sizes."
157+
158+ for line in Path (preprocessed_file ).read_text ().split ('\n ' ):
159+ if not line .startswith ('#' ) and line :
160+ print (line )
161+ print (preprocessed_file )
162+
157163 output = subprocess .check_output ([exe_file ], text = True )
158164 return output
159165
160166
161167@pytest .mark .skipif (os .name == 'nt' , reason = "C compiler check not implemented for Windows" )
162168def test_struct_layout ():
163169 output = c_run ("verify_structs" , C_CHECKER )
164- c_infos = {}
165- for line in output .strip ().split ('\n ' ):
166- name , value = line .split (':' )
167- c_infos [name ] = int (value )
170+ c_infos = {line .split (':' , 1 )[0 ]: int (line .split (':' , 1 )[1 ]) for line in output .strip ().split ('\n ' )}
171+ pprint .pprint (c_infos )
168172
169173 fail = False
170- for struct_name in STRUCT_NAMES :
171- key = f"{ struct_name } _size "
172- if c_infos [key ] == py_infos [key ]:
174+ for struct_name , member_names in STRUCT_NAMES . items () :
175+ key = f"{ struct_name } size "
176+ if c_infos [key ] == PY_INFOS [key ]:
173177 print (f"OK: { key } = { c_infos [key ]} " )
174178 else :
175- print (f"Mismatch for { key } : C={ c_infos [key ]} , Python={ py_infos [key ]} " )
179+ print (f"Mismatch for { key } : C={ c_infos [key ]} , Python={ PY_INFOS [key ]} " )
176180 fail = True
177- struct_members = [
178- (key , value )
179- for key , value in c_infos .items ()
180- if key .startswith (struct_name + '_' ) and key .endswith ("_offset" )
181- ]
182- for key , _ in sorted (struct_members , key = lambda x : x [1 ]): # sort by offset
183- if c_infos [key ] == py_infos [key ]:
181+
182+ for name in member_names :
183+ key = f"{ struct_name } .{ name } offset"
184+ if c_infos [key ] == PY_INFOS [key ]:
184185 print (f"OK: { key } = { c_infos [key ]} " )
185186 else :
186- print (f"Mismatch for { key } : C={ c_infos [key ]} , Python={ py_infos [key ]} " )
187+ print (f"Mismatch for { key } : C={ c_infos [key ]} , Python={ PY_INFOS [key ]} " )
187188 fail = True
189+
188190 assert not fail , "Struct layout mismatch, see stdout output for details!"
0 commit comments