Merge pull request #3850 from mesonbuild/nirbheek/exe-wrapper-compiler-fallbacks

Be more permissive about not-found exe_wrapper
pull/3964/head
Jussi Pakkanen 6 years ago committed by GitHub
commit e75f6e4305
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      mesonbuild/backend/backends.py
  2. 17
      mesonbuild/backend/ninjabackend.py
  3. 3
      mesonbuild/backend/vs2010backend.py
  4. 8
      mesonbuild/compilers/c.py
  5. 2
      mesonbuild/dependencies/__init__.py
  6. 26
      mesonbuild/dependencies/base.py
  7. 19
      mesonbuild/environment.py
  8. 4
      mesonbuild/interpreter.py
  9. 6
      mesonbuild/mesonlib.py
  10. 25
      mesonbuild/mtest.py
  11. 12
      mesonbuild/scripts/meson_exe.py
  12. 34
      run_tests.py
  13. 65
      run_unittests.py
  14. 20
      test cases/unit/35 exe_wrapper behaviour/broken-cross.txt
  15. 19
      test cases/unit/35 exe_wrapper behaviour/meson.build
  16. 2
      test cases/unit/35 exe_wrapper behaviour/meson_options.txt
  17. 17
      test cases/unit/35 exe_wrapper behaviour/prog.c

@ -72,6 +72,8 @@ class ExecutableSerialisation:
self.cmd_args = cmd_args
self.env = env
self.is_cross = is_cross
if exe_wrapper is not None:
assert(isinstance(exe_wrapper, dependencies.ExternalProgram))
self.exe_runner = exe_wrapper
self.workdir = workdir
self.extra_paths = extra_paths
@ -85,6 +87,8 @@ class TestSerialisation:
self.suite = suite
self.fname = fname
self.is_cross_built = is_cross_built
if exe_wrapper is not None:
assert(isinstance(exe_wrapper, dependencies.ExternalProgram))
self.exe_runner = exe_wrapper
self.is_parallel = is_parallel
self.cmd_args = cmd_args
@ -272,8 +276,11 @@ class Backend:
raise MesonException('Unknown data type in object list.')
return obj_list
def serialize_executable(self, exe, cmd_args, workdir, env={},
def serialize_executable(self, tname, exe, cmd_args, workdir, env={},
extra_paths=None, capture=None):
'''
Serialize an executable for running with a generator or a custom target
'''
import hashlib
if extra_paths is None:
# The callee didn't check if we needed extra paths, so check it here
@ -298,19 +305,24 @@ class Backend:
with open(exe_data, 'wb') as f:
if isinstance(exe, dependencies.ExternalProgram):
exe_cmd = exe.get_command()
exe_needs_wrapper = False
exe_is_native = True
elif isinstance(exe, (build.BuildTarget, build.CustomTarget)):
exe_cmd = [self.get_target_filename_abs(exe)]
exe_needs_wrapper = exe.is_cross
exe_is_native = not exe.is_cross
else:
exe_cmd = [exe]
exe_needs_wrapper = False
is_cross_built = exe_needs_wrapper and \
exe_is_native = True
is_cross_built = (not exe_is_native) and \
self.environment.is_cross_build() and \
self.environment.cross_info.need_cross_compiler() and \
self.environment.cross_info.need_exe_wrapper()
if is_cross_built:
exe_wrapper = self.environment.cross_info.config['binaries'].get('exe_wrapper', None)
exe_wrapper = self.environment.get_exe_wrapper()
if not exe_wrapper.found():
msg = 'The exe_wrapper {!r} defined in the cross file is ' \
'needed by target {!r}, but was not found. Please ' \
'check the command and/or add it to PATH.'
raise MesonException(msg.format(exe_wrapper.name, tname))
else:
exe_wrapper = None
es = ExecutableSerialisation(basename, exe_cmd, cmd_args, env,
@ -646,10 +658,10 @@ class Backend:
is_cross = is_cross and exe.is_cross
if isinstance(exe, dependencies.ExternalProgram):
# E.g. an external verifier or simulator program run on a generated executable.
# Can always be run.
# Can always be run without a wrapper.
is_cross = False
if is_cross:
exe_wrapper = self.environment.cross_info.config['binaries'].get('exe_wrapper', None)
exe_wrapper = self.environment.get_exe_wrapper()
else:
exe_wrapper = None
if mesonlib.for_windows(is_cross, self.environment) or \
@ -711,9 +723,8 @@ class Backend:
def exe_object_to_cmd_array(self, exe):
if self.environment.is_cross_build() and \
self.environment.cross_info.need_exe_wrapper() and \
isinstance(exe, build.BuildTarget) and exe.is_cross:
if 'exe_wrapper' not in self.environment.cross_info.config['binaries']:
if self.environment.exe_wrapper is None:
s = 'Can not use target %s as a generator because it is cross-built\n'
s += 'and no exe wrapper is defined. You might want to set it to native instead.'
s = s % exe.name

@ -545,7 +545,7 @@ int dummy;
if extra_paths:
serialize = True
if serialize:
exe_data = self.serialize_executable(target.command[0], cmd[1:],
exe_data = self.serialize_executable(target.name, target.command[0], cmd[1:],
# All targets are built from the build dir
self.environment.get_build_dir(),
extra_paths=extra_paths,
@ -599,11 +599,15 @@ int dummy;
if isinstance(texe, build.Executable):
abs_exe = os.path.join(self.environment.get_build_dir(), self.get_target_filename(texe))
deps.append(self.get_target_filename(texe))
if self.environment.is_cross_build() and \
self.environment.cross_info.need_exe_wrapper():
exe_wrap = self.environment.cross_info.config['binaries'].get('exe_wrapper', None)
if exe_wrap is not None:
cmd += [exe_wrap]
if self.environment.is_cross_build():
exe_wrap = self.environment.get_exe_wrapper()
if exe_wrap:
if not exe_wrap.found():
msg = 'The exe_wrapper {!r} defined in the cross file is ' \
'needed by run target {!r}, but was not found. ' \
'Please check the command and/or add it to PATH.'
raise MesonException(msg.format(exe_wrap.name, target.name))
cmd += exe_wrap.get_command()
cmd.append(abs_exe)
elif isinstance(texe, dependencies.ExternalProgram):
cmd += texe.get_command()
@ -1718,6 +1722,7 @@ rule FORTRAN_DEP_HACK%s
cmdlist = exe_arr + self.replace_extra_args(args, genlist)
if generator.capture:
exe_data = self.serialize_executable(
'generator ' + cmdlist[0],
cmdlist[0],
cmdlist[1:],
self.environment.get_build_dir(),

@ -133,6 +133,7 @@ class Vs2010Backend(backends.Backend):
cmd = exe_arr + self.replace_extra_args(args, genlist)
if generator.capture:
exe_data = self.serialize_executable(
'generator ' + cmd[0],
cmd[0],
cmd[1:],
self.environment.get_build_dir(),
@ -498,7 +499,7 @@ class Vs2010Backend(backends.Backend):
tdir_abs = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target))
extra_bdeps = target.get_transitive_build_target_deps()
extra_paths = self.determine_windows_extra_paths(target.command[0], extra_bdeps)
exe_data = self.serialize_executable(target.command[0], cmd[1:],
exe_data = self.serialize_executable(target.name, target.command[0], cmd[1:],
# All targets run from the target dir
tdir_abs,
extra_paths=extra_paths,

@ -60,10 +60,12 @@ class CCompiler(Compiler):
self.id = 'unknown'
self.is_cross = is_cross
self.can_compile_suffixes.add('h')
if isinstance(exe_wrapper, str):
self.exe_wrapper = [exe_wrapper]
# If the exe wrapper was not found, pretend it wasn't set so that the
# sanity check is skipped and compiler checks use fallbacks.
if not exe_wrapper or not exe_wrapper.found():
self.exe_wrapper = None
else:
self.exe_wrapper = exe_wrapper
self.exe_wrapper = exe_wrapper.get_command()
# Set to None until we actually need to check this
self.has_fatal_warnings_link_arg = None

@ -14,7 +14,7 @@
from .boost import BoostDependency
from .base import ( # noqa: F401
Dependency, DependencyException, DependencyMethods, ExternalProgram, NonExistingExternalProgram,
Dependency, DependencyException, DependencyMethods, ExternalProgram, EmptyExternalProgram, NonExistingExternalProgram,
ExternalDependency, NotFoundDependency, ExternalLibrary, ExtraFrameworkDependency, InternalDependency,
PkgConfigDependency, find_external_dependency, get_dep_identifier, packages, _packages_accept_language)
from .dev import GMockDependency, GTestDependency, LLVMDependency, ValgrindDependency

@ -1139,10 +1139,14 @@ class ExternalProgram:
def get_name(self):
return self.name
class NonExistingExternalProgram(ExternalProgram):
"A program that will never exist"
def __init__(self):
super().__init__(name = 'nonexistingprogram', silent = True)
self.name = 'nonexistingprogram'
self.command = [None]
self.path = None
def __repr__(self):
r = '<{} {!r} -> {!r}>'
@ -1151,6 +1155,26 @@ class NonExistingExternalProgram(ExternalProgram):
def found(self):
return False
class EmptyExternalProgram(ExternalProgram):
'''
A program object that returns an empty list of commands. Used for cases
such as a cross file exe_wrapper to represent that it's not required.
'''
def __init__(self):
self.name = None
self.command = []
self.path = None
def __repr__(self):
r = '<{} {!r} -> {!r}>'
return r.format(self.__class__.__name__, self.name, self.command)
def found(self):
return True
class ExternalLibrary(ExternalDependency):
def __init__(self, name, link_args, environment, language, silent=False):
super().__init__('library', environment, language, {})

@ -311,10 +311,13 @@ class Environment:
# Used by the regenchecker script, which runs meson
self.coredata.meson_command = mesonlib.meson_command
self.first_invocation = True
self.cross_info = None
self.exe_wrapper = None
if self.coredata.cross_file:
self.cross_info = CrossBuildInfo(self.coredata.cross_file)
else:
self.cross_info = None
if 'exe_wrapper' in self.cross_info.config['binaries']:
from .dependencies import ExternalProgram
self.exe_wrapper = ExternalProgram.from_cross_info(self.cross_info, 'exe_wrapper')
self.cmd_line_options = options.cmd_line_options.copy()
# List of potential compilers.
@ -480,10 +483,7 @@ This is probably wrong, it should always point to the native compiler.''' % evar
# Return value has to be a list of compiler 'choices'
compilers = [compilers]
is_cross = True
if self.cross_info.need_exe_wrapper():
exe_wrap = self.cross_info.config['binaries'].get('exe_wrapper', None)
else:
exe_wrap = []
exe_wrap = self.get_exe_wrapper()
elif evar in os.environ:
compilers = shlex.split(os.environ[evar])
# Ensure ccache exists and remove it if it doesn't
@ -973,6 +973,13 @@ This is probably wrong, it should always point to the native compiler.''' % evar
out = out.split('\n')[index].lstrip('libraries: =').split(':')
return [os.path.normpath(p) for p in out]
def get_exe_wrapper(self):
if not self.cross_info.need_exe_wrapper():
from .dependencies import EmptyExternalProgram
return EmptyExternalProgram()
return self.exe_wrapper
class CrossBuildInfo:
def __init__(self, filename):
self.config = {'properties': {}}

@ -1681,10 +1681,8 @@ class MesonMain(InterpreterObject):
@permittedKwargs({})
def has_exe_wrapper_method(self, args, kwargs):
if self.is_cross_build_method(None, None) and \
'binaries' in self.build.environment.cross_info.config and \
self.build.environment.cross_info.need_exe_wrapper():
exe_wrap = self.build.environment.cross_info.config['binaries'].get('exe_wrapper', None)
if exe_wrap is None:
if self.build.environment.exe_wrapper is None:
return False
# We return True when exe_wrap is defined, when it's not needed, and
# when we're compiling natively. The last two are semantically confusing.

@ -1069,6 +1069,12 @@ def detect_subprojects(spdir_name, current_dir='', result=None):
def get_error_location_string(fname, lineno):
return '{}:{}:'.format(fname, lineno)
def substring_is_in_list(substr, strlist):
for s in strlist:
if substr in s:
return True
return False
class OrderedSet(collections.MutableSet):
"""A set that preserves the order in which items are added, by first
insertion.

@ -240,7 +240,11 @@ class SingleTestRunner:
# because there is no execute wrapper.
return None
else:
return [self.test.exe_runner] + self.test.fname
if not self.test.exe_runner.found():
msg = 'The exe_wrapper defined in the cross file {!r} was not ' \
'found. Please check the command and/or add it to PATH.'
raise TestException(msg.format(self.test.exe_runner.name))
return self.test.exe_runner.get_command() + self.test.fname
else:
return self.test.fname
@ -257,19 +261,12 @@ class SingleTestRunner:
self.test.timeout = None
return self._run_cmd(wrap + cmd + self.test.cmd_args + self.options.test_args)
@staticmethod
def _substring_in_list(substr, strlist):
for s in strlist:
if substr in s:
return True
return False
def _run_cmd(self, cmd):
starttime = time.time()
if len(self.test.extra_paths) > 0:
self.env['PATH'] = os.pathsep.join(self.test.extra_paths + ['']) + self.env['PATH']
if self._substring_in_list('wine', cmd):
if mesonlib.substring_is_in_list('wine', cmd):
wine_paths = ['Z:' + p for p in self.test.extra_paths]
wine_path = ';'.join(wine_paths)
# Don't accidentally end with an `;` because that will add the
@ -744,12 +741,13 @@ def run(args):
if check_bin is not None:
exe = ExternalProgram(check_bin, silent=True)
if not exe.found():
sys.exit('Could not find requested program: %s' % check_bin)
print('Could not find requested program: {!r}'.format(check_bin))
return 1
options.wd = os.path.abspath(options.wd)
if not options.list and not options.no_rebuild:
if not rebuild_all(options.wd):
sys.exit(-1)
return 1
try:
th = TestHarness(options)
@ -761,5 +759,8 @@ def run(args):
return th.run_special()
except TestException as e:
print('Meson test encountered an error:\n')
print(e)
if os.environ.get('MESON_FORCE_BACKTRACE'):
raise e
else:
print(e)
return 1

@ -19,6 +19,8 @@ import pickle
import platform
import subprocess
from .. import mesonlib
options = None
def buildparser():
@ -47,9 +49,13 @@ def run_exe(exe):
else:
if exe.is_cross:
if exe.exe_runner is None:
raise AssertionError('BUG: Trying to run cross-compiled exes with no wrapper')
raise AssertionError('BUG: Can\'t run cross-compiled exe {!r}'
'with no wrapper'.format(exe.name))
elif not exe.exe_runner.found():
raise AssertionError('BUG: Can\'t run cross-compiled exe {!r} with not-found'
'wrapper {!r}'.format(exe.name, exe.exe_runner.get_path()))
else:
cmd = [exe.exe_runner] + exe.fname
cmd = exe.exe_runner.get_command() + exe.fname
else:
cmd = exe.fname
child_env = os.environ.copy()
@ -57,7 +63,7 @@ def run_exe(exe):
if len(exe.extra_paths) > 0:
child_env['PATH'] = (os.pathsep.join(exe.extra_paths + ['']) +
child_env['PATH'])
if exe.exe_runner and 'wine' in exe.exe_runner:
if exe.exe_runner and mesonlib.substring_is_in_list('wine', exe.exe_runner.get_command()):
wine_paths = ['Z:' + p for p in exe.extra_paths]
wine_path = ';'.join(wine_paths)
# Don't accidentally end with an `;` because that will add the

@ -29,6 +29,7 @@ from pathlib import Path
import mesonbuild
from mesonbuild import mesonlib
from mesonbuild import mesonmain
from mesonbuild import mtest
from mesonbuild import mlog
from mesonbuild.environment import detect_ninja
@ -156,8 +157,17 @@ def get_fake_options(prefix):
opts.cmd_line_options = {}
return opts
def should_run_linux_cross_tests():
return shutil.which('arm-linux-gnueabihf-gcc') and not platform.machine().lower().startswith('arm')
def run_mtest_inprocess(commandlist):
old_stdout = sys.stdout
sys.stdout = mystdout = StringIO()
old_stderr = sys.stderr
sys.stderr = mystderr = StringIO()
try:
returncode = mtest.run(commandlist)
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr
return returncode, mystdout.getvalue(), mystderr.getvalue()
def run_configure_inprocess(commandlist):
old_stdout = sys.stdout
@ -203,7 +213,7 @@ if __name__ == '__main__':
# Iterate over list in reverse order to find the last --backend arg
backend = Backend.ninja
cross = False
# FIXME: Convert to argparse
# FIXME: PLEASE convert to argparse
for arg in reversed(sys.argv[1:]):
if arg.startswith('--backend'):
if arg.startswith('--backend=vs'):
@ -212,6 +222,10 @@ if __name__ == '__main__':
backend = Backend.xcode
if arg.startswith('--cross'):
cross = True
if arg == '--cross=mingw':
cross = 'mingw'
elif arg == '--cross=arm':
cross = 'arm'
# Running on a developer machine? Be nice!
if not mesonlib.is_windows() and not mesonlib.is_haiku() and 'TRAVIS' not in os.environ:
os.nice(20)
@ -249,10 +263,12 @@ if __name__ == '__main__':
returncode += subprocess.call(mesonlib.python_command + ['run_project_tests.py'] + sys.argv[1:], env=env)
else:
cross_test_args = mesonlib.python_command + ['run_cross_test.py']
print(mlog.bold('Running armhf cross tests.').get_text(mlog.colorize_console))
print()
returncode += subprocess.call(cross_test_args + ['cross/ubuntu-armhf.txt'], env=env)
print(mlog.bold('Running mingw-w64 64-bit cross tests.').get_text(mlog.colorize_console))
print()
returncode += subprocess.call(cross_test_args + ['cross/linux-mingw-w64-64bit.txt'], env=env)
if cross is True or cross == 'arm':
print(mlog.bold('Running armhf cross tests.').get_text(mlog.colorize_console))
print()
returncode += subprocess.call(cross_test_args + ['cross/ubuntu-armhf.txt'], env=env)
if cross is True or cross == 'mingw':
print(mlog.bold('Running mingw-w64 64-bit cross tests.').get_text(mlog.colorize_console))
print()
returncode += subprocess.call(cross_test_args + ['cross/linux-mingw-w64-64bit.txt'], env=env)
sys.exit(returncode)

@ -22,6 +22,7 @@ import textwrap
import os
import shutil
import unittest
import platform
from unittest import mock
from configparser import ConfigParser
from glob import glob
@ -47,7 +48,7 @@ import mesonbuild.modules.pkgconfig
from run_tests import exe_suffix, get_fake_options, get_meson_script
from run_tests import get_builddir_target_args, get_backend_commands, Backend
from run_tests import ensure_backend_detects_changes, run_configure_inprocess
from run_tests import should_run_linux_cross_tests
from run_tests import run_mtest_inprocess
def get_dynamic_section_entry(fname, entry):
@ -746,8 +747,11 @@ class BasePlatformTests(unittest.TestCase):
dir_args = get_builddir_target_args(self.backend, self.builddir, None)
self._run(self.clean_command + dir_args, workdir=self.builddir)
def run_tests(self):
self._run(self.test_command, workdir=self.builddir)
def run_tests(self, inprocess=False):
if not inprocess:
self._run(self.test_command, workdir=self.builddir)
else:
run_mtest_inprocess(['-C', self.builddir])
def install(self, *, use_destdir=True):
if self.backend is not Backend.ninja:
@ -3674,9 +3678,9 @@ endian = 'little'
self.assertNotRegex(out, self.installdir + '.*dylib ')
class LinuxArmCrossCompileTests(BasePlatformTests):
class LinuxCrossArmTests(BasePlatformTests):
'''
Tests that verify cross-compilation to Linux/ARM
Tests that cross-compilation to Linux/ARM works
'''
def setUp(self):
super().setUp()
@ -3710,6 +3714,45 @@ class LinuxArmCrossCompileTests(BasePlatformTests):
self.assertRegex(compdb[0]['command'], '-D_FILE_OFFSET_BITS=64.*-U_FILE_OFFSET_BITS')
self.build()
class LinuxCrossMingwTests(BasePlatformTests):
'''
Tests that cross-compilation to Windows/MinGW works
'''
def setUp(self):
super().setUp()
src_root = os.path.dirname(__file__)
self.meson_cross_file = os.path.join(src_root, 'cross', 'linux-mingw-w64-64bit.txt')
def test_exe_wrapper_behaviour(self):
'''
Test that an exe wrapper that isn't found doesn't cause compiler sanity
checks and compiler checks to fail, but causes configure to fail if it
requires running a cross-built executable (custom_target or run_target)
and causes the tests to be skipped if they are run.
'''
testdir = os.path.join(self.unit_test_dir, '35 exe_wrapper behaviour')
# Configures, builds, and tests fine by default
self.init(testdir)
self.build()
self.run_tests()
self.wipe()
os.mkdir(self.builddir)
# Change cross file to use a non-existing exe_wrapper and it should fail
self.meson_cross_file = os.path.join(testdir, 'broken-cross.txt')
# Force tracebacks so we can detect them properly
os.environ['MESON_FORCE_BACKTRACE'] = '1'
with self.assertRaisesRegex(MesonException, 'exe_wrapper.*target.*use-exe-wrapper'):
# Must run in-process or we'll get a generic CalledProcessError
self.init(testdir, extra_args='-Drun-target=false', inprocess=True)
with self.assertRaisesRegex(MesonException, 'exe_wrapper.*run target.*run-prog'):
# Must run in-process or we'll get a generic CalledProcessError
self.init(testdir, extra_args='-Dcustom-target=false', inprocess=True)
self.init(testdir, extra_args=['-Dcustom-target=false', '-Drun-target=false'])
self.build()
with self.assertRaisesRegex(MesonException, 'exe_wrapper.*PATH'):
# Must run in-process or we'll get a generic CalledProcessError
self.run_tests(inprocess=True)
class PythonTests(BasePlatformTests):
'''
@ -3843,13 +3886,21 @@ def unset_envs():
if v in os.environ:
del os.environ[v]
def should_run_cross_arm_tests():
return shutil.which('arm-linux-gnueabihf-gcc') and not platform.machine().lower().startswith('arm')
def should_run_cross_mingw_tests():
return shutil.which('x86_64-w64-mingw32-gcc') and not (is_windows() or is_cygwin())
if __name__ == '__main__':
unset_envs()
cases = ['InternalTests', 'AllPlatformTests', 'FailureTests', 'PythonTests']
if not is_windows():
cases += ['LinuxlikeTests']
if should_run_linux_cross_tests():
cases += ['LinuxArmCrossCompileTests']
if should_run_cross_arm_tests():
cases += ['LinuxCrossArmTests']
if should_run_cross_mingw_tests():
cases += ['LinuxCrossMingwTests']
if is_windows() or is_cygwin():
cases += ['WindowsTests']

@ -0,0 +1,20 @@
[binaries]
c = '/usr/bin/x86_64-w64-mingw32-gcc'
cpp = '/usr/bin/x86_64-w64-mingw32-g++'
ar = '/usr/bin/x86_64-w64-mingw32-ar'
strip = '/usr/bin/x86_64-w64-mingw32-strip'
pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config'
windres = '/usr/bin/x86_64-w64-mingw32-windres'
exe_wrapper = 'broken'
[properties]
# Directory that contains 'bin', 'lib', etc
root = '/usr/x86_64-w64-mingw32'
# Directory that contains 'bin', 'lib', etc for the toolchain and system libraries
sys_root = '/usr/x86_64-w64-mingw32/sys-root/mingw'
[host_machine]
system = 'windows'
cpu_family = 'x86_64'
cpu = 'x86_64'
endian = 'little'

@ -0,0 +1,19 @@
project('exe wrapper behaviour', 'c')
assert(meson.is_cross_build(), 'not setup as cross build')
assert(meson.has_exe_wrapper(), 'exe wrapper not defined?')
exe = executable('prog', 'prog.c')
if get_option('custom-target')
custom_target('use-exe-wrapper',
build_by_default: true,
output: 'out.txt',
command: [exe, '@OUTPUT@'])
endif
test('test-prog', exe)
if get_option('run-target')
run_target('run-prog', command : exe)
endif

@ -0,0 +1,2 @@
option('custom-target', type: 'boolean', value: true)
option('run-target', type: 'boolean', value: true)

@ -0,0 +1,17 @@
#include <stdio.h>
int main (int argc, char * argv[])
{
const char *out = "SUCCESS!";
if (argc != 2) {
printf ("%s\n", out);
} else {
int ret;
FILE *f = fopen (argv[1], "w");
ret = fwrite (out, sizeof (out), 1, f);
if (ret != 1)
return -1;
}
return 0;
}
Loading…
Cancel
Save