1#
2# File      : mkdir.py
3# This file is part of RT-Thread RTOS
4# COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team
5#
6#  This program is free software; you can redistribute it and/or modify
7#  it under the terms of the GNU General Public License as published by
8#  the Free Software Foundation; either version 2 of the License, or
9#  (at your option) any later version.
10#
11#  This program is distributed in the hope that it will be useful,
12#  but WITHOUT ANY WARRANTY; without even the implied warranty of
13#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14#  GNU General Public License for more details.
15#
16#  You should have received a copy of the GNU General Public License along
17#  with this program; if not, write to the Free Software Foundation, Inc.,
18#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19#
20# Change Logs:
21# Date           Author       Notes
22# 2017-10-04     Bernard      The first version
23# 2025-01-07     ZhaoCake     components copy and gen doc
24# 2025-03-02     ZhaoCake     Add MkDist_Strip
25
26
27import os
28import subprocess
29import shutil
30from shutil import ignore_patterns
31from SCons.Script import *
32
33def do_copy_file(src, dst):
34    # check source file
35    if not os.path.exists(src):
36        return
37
38    path = os.path.dirname(dst)
39    # mkdir if path not exist
40    if not os.path.exists(path):
41        os.makedirs(path)
42
43    shutil.copy2(src, dst)
44
45def do_copy_folder(src_dir, dst_dir, ignore=None):
46    import shutil
47    # check source directory
48    if not os.path.exists(src_dir):
49        return
50
51    try:
52        if os.path.exists(dst_dir):
53            shutil.rmtree(dst_dir)
54    except:
55        print('Deletes folder: %s failed.' % dst_dir)
56        return
57
58    shutil.copytree(src_dir, dst_dir, ignore = ignore)
59
60source_ext = ['c', 'h', 's', 'S', 'cpp', 'cxx', 'cc', 'xpm']
61source_list = []
62
63def walk_children(child):
64    global source_list
65    global source_ext
66
67    # print child
68    full_path = child.rfile().abspath
69    file_type  = full_path.rsplit('.',1)[1]
70    #print file_type
71    if file_type in source_ext:
72        if full_path not in source_list:
73            source_list.append(full_path)
74
75    children = child.all_children()
76    if children != []:
77        for item in children:
78            walk_children(item)
79
80def walk_kconfig(RTT_ROOT, source_list):
81    for parent, dirnames, filenames in os.walk(RTT_ROOT):
82        if 'bsp' in parent:
83            continue
84        if '.git' in parent:
85            continue
86        if 'tools' in parent:
87            continue
88
89        if 'Kconfig' in filenames:
90            pathfile = os.path.join(parent, 'Kconfig')
91            source_list.append(pathfile)
92        if 'KConfig' in filenames:
93            pathfile = os.path.join(parent, 'KConfig')
94            source_list.append(pathfile)
95
96def bsp_copy_files(bsp_root, dist_dir):
97    # copy BSP files
98    do_copy_folder(os.path.join(bsp_root), dist_dir,
99        ignore_patterns('build', '__pycache__', 'dist', '*.pyc', '*.old', '*.map', 'rtthread.bin', '.sconsign.dblite', '*.elf', '*.axf', 'cconfig.h'))
100
101def bsp_update_sconstruct(dist_dir):
102    with open(os.path.join(dist_dir, 'SConstruct'), 'r') as f:
103        data = f.readlines()
104    with open(os.path.join(dist_dir, 'SConstruct'), 'w') as f:
105        for line in data:
106            if line.find('RTT_ROOT') != -1:
107                if line.find('sys.path') != -1:
108                    f.write('# set RTT_ROOT\n')
109                    f.write('if not os.getenv("RTT_ROOT"): \n    RTT_ROOT="rt-thread"\n\n')
110            f.write(line)
111
112def bsp_update_kconfig_testcases(dist_dir):
113    # delete testcases in rt-thread/Kconfig
114    if not os.path.isfile(os.path.join(dist_dir, 'rt-thread/Kconfig')):
115        return
116
117    with open(os.path.join(dist_dir, 'rt-thread/Kconfig'), 'r') as f:
118        data = f.readlines()
119    with open(os.path.join(dist_dir, 'rt-thread/Kconfig'), 'w') as f:
120        for line in data:
121            if line.find('examples/utest/testcases/Kconfig') == -1:
122                f.write(line)
123
124def bsp_update_kconfig(dist_dir):
125    # change RTT_ROOT in Kconfig
126    if not os.path.isfile(os.path.join(dist_dir, 'Kconfig')):
127        return
128
129    with open(os.path.join(dist_dir, 'Kconfig'), 'r') as f:
130        data = f.readlines()
131    with open(os.path.join(dist_dir, 'Kconfig'), 'w') as f:
132        for line in data:
133            if line.find('RTT_DIR') != -1 and line.find(':=') != -1:
134                line = 'RTT_DIR := rt-thread\n'
135            f.write(line)
136
137def bsp_update_kconfig_library(dist_dir):
138    # change RTT_ROOT in Kconfig
139    if not os.path.isfile(os.path.join(dist_dir, 'Kconfig')):
140        return
141
142    with open(os.path.join(dist_dir, 'Kconfig'), 'r') as f:
143        data = f.readlines()
144    with open(os.path.join(dist_dir, 'Kconfig'), 'w') as f:
145        for line in data:
146            if line.find('source') != -1 and line.find('../libraries') != -1:
147                line = line.replace('../libraries', 'libraries')
148            f.write(line)
149
150    # change board/kconfig path
151    if not os.path.isfile(os.path.join(dist_dir, 'board/Kconfig')):
152        return
153
154    with open(os.path.join(dist_dir, 'board/Kconfig'), 'r') as f:
155        data = f.readlines()
156    with open(os.path.join(dist_dir, 'board/Kconfig'), 'w') as f:
157        for line in data:
158            if line.find('source') != -1 and line.find('../libraries') != -1:
159                line = line.replace('../libraries', 'libraries')
160            f.write(line)
161
162def zip_dist(dist_dir, dist_name):
163    import zipfile
164
165    zip_filename = os.path.join(dist_dir)
166    zip = zipfile.ZipFile(zip_filename + '.zip', 'w')
167    pre_len = len(os.path.dirname(dist_dir))
168
169    for parent, dirnames, filenames in os.walk(dist_dir):
170        for filename in filenames:
171            pathfile = os.path.join(parent, filename)
172            arcname = pathfile[pre_len:].strip(os.path.sep)
173            zip.write(pathfile, arcname)
174
175    zip.close()
176
177def MkDist(program, BSP_ROOT, RTT_ROOT, Env, project_name, project_path):
178    print('make distribution....')
179
180    if project_path == None:
181        dist_dir = os.path.join(BSP_ROOT, 'dist', project_name)
182    else:
183        dist_dir = project_path
184
185    rtt_dir_path = os.path.join(dist_dir, 'rt-thread')
186
187    # copy BSP files
188    print('=> %s' % os.path.basename(BSP_ROOT))
189    bsp_copy_files(BSP_ROOT, dist_dir)
190
191    # do bsp special dist handle
192    if 'dist_handle' in Env:
193        print("=> start dist handle")
194        dist_handle = Env['dist_handle']
195        dist_handle(BSP_ROOT, dist_dir)
196
197    # copy tools directory
198    print('=> components')
199    do_copy_folder(os.path.join(RTT_ROOT, 'components'), os.path.join(rtt_dir_path, 'components'))
200
201    # skip documentation directory
202    # skip examples
203
204    # copy include directory
205    print('=> include')
206    do_copy_folder(os.path.join(RTT_ROOT, 'include'), os.path.join(rtt_dir_path, 'include'))
207
208    # copy all libcpu/ARCH directory
209    print('=> libcpu')
210    import rtconfig
211    do_copy_folder(os.path.join(RTT_ROOT, 'libcpu', rtconfig.ARCH), os.path.join(rtt_dir_path, 'libcpu', rtconfig.ARCH))
212    do_copy_file(os.path.join(RTT_ROOT, 'libcpu', 'Kconfig'), os.path.join(rtt_dir_path, 'libcpu', 'Kconfig'))
213    do_copy_file(os.path.join(RTT_ROOT, 'libcpu', 'SConscript'), os.path.join(rtt_dir_path, 'libcpu', 'SConscript'))
214
215    # copy src directory
216    print('=> src')
217    do_copy_folder(os.path.join(RTT_ROOT, 'src'), os.path.join(rtt_dir_path, 'src'))
218
219    # copy tools directory
220    print('=> tools')
221    do_copy_folder(os.path.join(RTT_ROOT, 'tools'), os.path.join(rtt_dir_path, 'tools'), ignore_patterns('*.pyc'))
222
223    do_copy_file(os.path.join(RTT_ROOT, 'Kconfig'), os.path.join(rtt_dir_path, 'Kconfig'))
224    do_copy_file(os.path.join(RTT_ROOT, 'AUTHORS'), os.path.join(rtt_dir_path, 'AUTHORS'))
225    do_copy_file(os.path.join(RTT_ROOT, 'COPYING'), os.path.join(rtt_dir_path, 'COPYING'))
226    do_copy_file(os.path.join(RTT_ROOT, 'README.md'), os.path.join(rtt_dir_path, 'README.md'))
227    do_copy_file(os.path.join(RTT_ROOT, 'README_zh.md'), os.path.join(rtt_dir_path, 'README_zh.md'))
228
229    print('Update configuration files...')
230    # change RTT_ROOT in SConstruct
231    bsp_update_sconstruct(dist_dir)
232    # change RTT_ROOT in Kconfig
233    bsp_update_kconfig(dist_dir)
234    bsp_update_kconfig_library(dist_dir)
235    # delete testcases in Kconfig
236    bsp_update_kconfig_testcases(dist_dir)
237
238    target_project_type = GetOption('target')
239    if target_project_type:
240        child = subprocess.Popen('scons --target={} --project-name="{}"'.format(target_project_type, project_name), cwd=dist_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
241        stdout, stderr = child.communicate()
242        if child.returncode == 0:
243            print(stdout)
244        else:
245            print(stderr)
246    else:
247        print('suggest to use command scons --dist [--target=xxx] [--project-name="xxx"] [--project-path="xxx"]')
248
249    # make zip package
250    if project_path == None:
251        zip_dist(dist_dir, project_name)
252
253    print('dist project successfully!')
254
255def MkDist_Strip(program, BSP_ROOT, RTT_ROOT, env, project_name, project_path=None):
256    """Create a minimal distribution based on compile_commands.json but keeping all build system files.
257    First copies everything like MkDist, then only removes unused source files while keeping all headers.
258    """
259    print('Making minimal distribution for project...')
260
261    if project_path == None:
262        dist_dir = os.path.join(BSP_ROOT, 'dist', project_name)
263    else:
264        dist_dir = project_path
265
266    # First do a full distribution copy
267    MkDist(program, BSP_ROOT, RTT_ROOT, env, project_name, project_path)
268    print('\n=> Starting source files cleanup...')
269
270    # Get the minimal required source paths
271    import compile_commands
272    used_paths = compile_commands.get_minimal_dist_paths(
273        os.path.join(BSP_ROOT, 'compile_commands.json'),
274        RTT_ROOT
275    )
276
277    # Clean up RT-Thread directory except tools and build files
278    rt_thread_dir = os.path.join(dist_dir, 'rt-thread')
279    source_extensions = ('.c', '.cpp', '.cxx', '.cc', '.s', '.S')
280
281    removed_files = []
282    removed_dirs = []
283
284    for root, dirs, files in os.walk(rt_thread_dir, topdown=False):
285        rel_path = os.path.relpath(root, rt_thread_dir)
286
287        if rel_path.startswith('tools') or rel_path.startswith('include'):
288            continue
289
290        keep_files = {
291            'SConscript',
292            'Kconfig',
293            'Sconscript',
294            '.config',
295            'rtconfig.h'
296        }
297
298        for f in files:
299            if f in keep_files:
300                continue
301
302            if not f.endswith(source_extensions):
303                continue
304
305            file_path = os.path.join(root, f)
306            rel_file_path = os.path.relpath(file_path, rt_thread_dir)
307            dir_name = os.path.dirname(rel_file_path)
308
309            if dir_name not in used_paths and rel_file_path not in used_paths:
310                os.remove(file_path)
311                removed_files.append(rel_file_path)
312
313        # Remove empty directories
314        try:
315            if not os.listdir(root):
316                os.rmdir(root)
317                removed_dirs.append(rel_path)
318        except:
319            pass
320
321    # Output summary
322    if removed_files:
323        print("Removed {} unused source files".format(len(removed_files)))
324        log_file = os.path.join(dist_dir, 'cleanup.log')
325        with open(log_file, 'w') as f:
326            f.write("Removed source files:\n")
327            f.write('\n'.join(removed_files))
328            if removed_dirs:
329                f.write("\n\nRemoved empty directories:\n")
330                f.write('\n'.join(removed_dirs))
331        print("Details have been written to {}".format(log_file))
332    else:
333        print("No unused source files found")
334
335    # Make zip package like MkDist
336    if project_path is None:
337        zip_dist(dist_dir, project_name)
338        print("Distribution package created: {}.zip".format(dist_dir))
339    print('=> Distribution stripped successfully')